From 7d7c240f4b0d1c2ef31fe73fc021a0802aec6454 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 3 Nov 2024 11:36:14 +1100 Subject: [PATCH 0001/2338] feat: let user select pr to checkout --- pkg/cmd/pr/checkout/checkout.go | 84 +++++++++++++++++++++++++++++--- pkg/cmd/pr/list/http.go | 2 +- pkg/cmd/pr/list/list.go | 2 +- pkg/cmd/pr/shared/commentable.go | 3 ++ pkg/cmd/pr/shared/display.go | 9 ++++ 5 files changed, 90 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index f566cbe1d66..5bb0cc113c1 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/pr/list" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -25,8 +26,10 @@ type CheckoutOptions struct { Remotes func() (cliContext.Remotes, error) Branch func() (string, error) - Finder shared.PRFinder + Finder shared.PRFinder + Prompter shared.Prompter + BaseRepo func() (ghrepo.Interface, error) SelectorArg string RecurseSubmodules bool Force bool @@ -42,12 +45,14 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr Config: f.Config, Remotes: f.Remotes, Branch: f.Branch, + Prompter: f.Prompter, + BaseRepo: f.BaseRepo, } cmd := &cobra.Command{ - Use: "checkout { | | }", + Use: "checkout [ | | ]", Short: "Check out a pull request in git", - Args: cmdutil.ExactArgs(1, "argument required"), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Finder = shared.NewFinder(f) @@ -71,15 +76,41 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr } func checkoutRun(opts *CheckoutOptions) error { - findOptions := shared.FindOptions{ - Selector: opts.SelectorArg, - Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"}, - } - pr, baseRepo, err := opts.Finder.Find(findOptions) + baseRepo, err := opts.BaseRepo() if err != nil { return err } + var pr *api.PullRequest + + if len(opts.SelectorArg) > 0 { + + findOptions := shared.FindOptions{ + Selector: opts.SelectorArg, + Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"}, + } + pr0, _, err := opts.Finder.Find(findOptions) + if err != nil { + return err + } + + pr = pr0 + + } else { + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + pr0, err := selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()) + if err != nil { + return err + } + + pr = pr0 + + } cfg, err := opts.Config() if err != nil { return err @@ -258,3 +289,40 @@ func executeCmds(client *git.Client, cmdQueue [][]string) error { } return nil } + +func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, cs *iostreams.ColorScheme) (*api.PullRequest, error) { + listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{ + "number", + "title", + "state", + "url", + "headRefName", + "headRepositoryOwner", + "isCrossRepository", + "isDraft", + "createdAt", + }}, 30) + if err != nil { + return nil, err + } + pr, err := promptForPR(prompter, cs, *listResult) + return pr, err +} + +func promptForPR(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { + candidates := []string{} + for _, pr := range jobs.PullRequests { + candidates = append(candidates, fmt.Sprintf("%s %s", shared.PRNumberTitleWithColor(cs, pr), pr.Title)) + } + + selected, err := prompter.Select("Check out a specific PR?", "", candidates) + if err != nil { + return nil, err + } + + if selected >= 0 { + return &jobs.PullRequests[selected], nil + } + + return nil, nil +} diff --git a/pkg/cmd/pr/list/http.go b/pkg/cmd/pr/list/http.go index 5887551cfb1..cb79b3b87db 100644 --- a/pkg/cmd/pr/list/http.go +++ b/pkg/cmd/pr/list/http.go @@ -13,7 +13,7 @@ func shouldUseSearch(filters prShared.FilterOptions) bool { return filters.Draft != nil || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0 } -func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { +func ListPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { if shouldUseSearch(filters) { return searchPullRequests(httpClient, repo, filters, limit) } diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 93a9b8b4b68..401e168e31a 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -172,7 +172,7 @@ func listRun(opts *ListOptions) error { return opts.Browser.Browse(openURL) } - listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults) + listResult, err := ListPullRequests(httpClient, baseRepo, filters, opts.LimitResults) if err != nil { return err } diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 7a38286d2ee..46a71a2b4af 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -19,6 +19,9 @@ import ( type InputType int +type Prompter interface { + Select(string, string, []string) (int, error) +} const ( InputTypeEditor InputType = iota InputTypeInline diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index 7bd306104c0..7920eac4e79 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -17,6 +17,15 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { return prStateColorFunc(text.Title(pr.State)) } +func PRNumberTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { + prStateColorFunc := cs.ColorFromString((ColorForPRState(pr))) + prNumber := fmt.Sprintf("%d", pr.Number) + if pr.State == "OPEN" && pr.IsDraft { + return prStateColorFunc(text.Title(prNumber)) + } + return prStateColorFunc(text.Title(prNumber)) +} + func ColorForPRState(pr api.PullRequest) string { switch pr.State { case "OPEN": From bfd140c0e5f11f7fcbcb4ddbbc2ba4e0624604c5 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 6 Nov 2024 07:57:18 -0700 Subject: [PATCH 0002/2338] initial pass at fetching bundles with sas urls Signed-off-by: Meredith Lancaster --- go.mod | 1 + go.sum | 2 + pkg/cmd/attestation/api/attestation.go | 1 + pkg/cmd/attestation/api/client.go | 49 +++++++++++++++++++ pkg/cmd/attestation/api/mock_client.go | 4 ++ .../attestation/verification/attestation.go | 16 +++++- 6 files changed, 71 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 77be0c19db6..9a2e53d38a7 100644 --- a/go.mod +++ b/go.mod @@ -92,6 +92,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/snappy v0.0.4 // 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 diff --git a/go.sum b/go.sum index fe7adc3e73b..cc92ed45919 100644 --- a/go.sum +++ b/go.sum @@ -198,6 +198,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME= 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= diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go index ea055b293b6..d5c444ee297 100644 --- a/pkg/cmd/attestation/api/attestation.go +++ b/pkg/cmd/attestation/api/attestation.go @@ -26,6 +26,7 @@ func newErrNoAttestations(name, digest string) ErrNoAttestations { type Attestation struct { Bundle *bundle.Bundle `json:"bundle"` + SASUrl string `json:"signedAccessSignatureUrl"` } type AttestationsResponse struct { diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index f47b5f759da..f2f9f48f865 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -1,16 +1,21 @@ package api import ( + "encoding/json" "errors" "fmt" "io" "net/http" + "net/url" "strings" "time" "github.com/cenkalti/backoff/v4" "github.com/cli/cli/v2/api" ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/golang/snappy" + v1 "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + "github.com/sigstore/sigstore-go/pkg/bundle" ) const ( @@ -25,6 +30,7 @@ type apiClient interface { } type Client interface { + FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) GetTrustDomain() (string, error) @@ -130,6 +136,49 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At return attestations, nil } +func (c *LiveClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { + fetched := make([]*Attestation, len(attestations)) + for i, a := range attestations { + b, err := c.fetchAttestationWithSASURL(a) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + } + fetched[i] = &Attestation{ + Bundle: b, + } + } + return fetched, nil +} + +func (c *LiveClient) fetchAttestationWithSASURL(a *Attestation) (*bundle.Bundle, error) { + if a.SASUrl == "" { + return a.Bundle, nil + } + + parsed, err := url.Parse(a.SASUrl) + if err != nil { + return nil, err + } + + var resp []byte + if err = c.api.REST(parsed.Host, http.MethodGet, parsed.Path, nil, &resp); err != nil { + return nil, err + } + + var decompressedBytes []byte + _, err = snappy.Decode(decompressedBytes, resp) + if err != nil { + return nil, err + } + + var pbBundle *v1.Bundle + if err = json.Unmarshal(decompressedBytes, pbBundle); err != nil { + return nil, err + } + + return bundle.NewBundle(pbBundle) +} + func shouldRetry(err error) bool { var httpError api.HTTPError if errors.As(err, &httpError) { diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go index bcb51c414f5..17bfdadeca6 100644 --- a/pkg/cmd/attestation/api/mock_client.go +++ b/pkg/cmd/attestation/api/mock_client.go @@ -12,6 +12,10 @@ type MockClient struct { OnGetTrustDomain func() (string, error) } +func (m MockClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { + return nil, nil +} + func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { return m.OnGetByRepoAndDigest(repo, digest, limit) } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 0ea91c2f7f0..a4548a72d08 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -127,13 +127,25 @@ func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Repo, err) } - return attestations, nil + + fetched, err := c.APIClient.FetchAttestationsWithSASURL(attestations) + if err != nil { + return nil, fmt.Errorf("failed to fetch bundles from SAS URL: %w", err) + } + + return fetched, nil } else if c.Owner != "" { attestations, err := c.APIClient.GetByOwnerAndDigest(c.Owner, c.Digest, c.Limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Owner, err) } - return attestations, nil + + fetched, err := c.APIClient.FetchAttestationsWithSASURL(attestations) + if err != nil { + return nil, fmt.Errorf("failed to fetch bundles from SAS URL: %w", err) + } + + return fetched, nil } return nil, fmt.Errorf("owner or repo must be provided") } From 4386fcb4e5d984ffbe4c276f63da9e8f39f4b7bc Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 17 Nov 2024 14:51:27 +1100 Subject: [PATCH 0003/2338] issue #2329: fix the unit tests issue - ensure its backward compatible --- pkg/cmd/pr/checkout/checkout.go | 42 +++++++++++++++++---------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 5bb0cc113c1..0c3cb257171 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -76,40 +76,42 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr } func checkoutRun(opts *CheckoutOptions) error { - baseRepo, err := opts.BaseRepo() - if err != nil { - return err - } - - var pr *api.PullRequest + var ( + baseRepo ghrepo.Interface + pr *api.PullRequest + err error + ) - if len(opts.SelectorArg) > 0 { - - findOptions := shared.FindOptions{ + switch { + case opts.SelectorArg != "": + pr, baseRepo, err = opts.Finder.Find(shared.FindOptions{ Selector: opts.SelectorArg, - Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"}, - } - pr0, _, err := opts.Finder.Find(findOptions) + Fields: []string{ + "number", + "headRefName", + "headRepository", + "headRepositoryOwner", + "isCrossRepository", + "maintainerCanModify", + }, + }) if err != nil { return err } - pr = pr0 - - } else { - + default: httpClient, err := opts.HttpClient() if err != nil { return err } - pr0, err := selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()) - if err != nil { + if baseRepo, err = opts.BaseRepo(); err != nil { return err } - pr = pr0 - + if pr, err = selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()); err != nil { + return err + } } cfg, err := opts.Config() if err != nil { From 1832c1a767bcb7c1bafeca134817ec54e9335d79 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 17 Nov 2024 14:21:21 +1100 Subject: [PATCH 0004/2338] issue #2329: fix the linting issue --- pkg/cmd/pr/checkout/checkout.go | 2 +- pkg/cmd/pr/list/http_test.go | 6 +++--- pkg/cmd/pr/list/list_test.go | 4 ++-- pkg/cmd/pr/shared/commentable.go | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 0c3cb257171..54c0cae93d8 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -88,7 +88,7 @@ func checkoutRun(opts *CheckoutOptions) error { Selector: opts.SelectorArg, Fields: []string{ "number", - "headRefName", + "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", diff --git a/pkg/cmd/pr/list/http_test.go b/pkg/cmd/pr/list/http_test.go index 95230245bf8..060e3a17924 100644 --- a/pkg/cmd/pr/list/http_test.go +++ b/pkg/cmd/pr/list/http_test.go @@ -10,7 +10,7 @@ import ( "github.com/cli/cli/v2/pkg/httpmock" ) -func Test_listPullRequests(t *testing.T) { +func Test_ListPullRequests(t *testing.T) { type args struct { repo ghrepo.Interface filters prShared.FilterOptions @@ -153,9 +153,9 @@ func Test_listPullRequests(t *testing.T) { } httpClient := &http.Client{Transport: reg} - _, err := listPullRequests(httpClient, tt.args.repo, tt.args.filters, tt.args.limit) + _, err := ListPullRequests(httpClient, tt.args.repo, tt.args.filters, tt.args.limit) if (err != nil) != tt.wantErr { - t.Errorf("listPullRequests() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ListPullRequests() error = %v, wantErr %v", err, tt.wantErr) return } }) diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go index ecd0326b508..7ee111afc1c 100644 --- a/pkg/cmd/pr/list/list_test.go +++ b/pkg/cmd/pr/list/list_test.go @@ -368,7 +368,7 @@ func TestPRList_withProjectItems(t *testing.T) { })) client := &http.Client{Transport: reg} - prsAndTotalCount, err := listPullRequests( + prsAndTotalCount, err := ListPullRequests( client, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{ @@ -437,7 +437,7 @@ func TestPRList_Search_withProjectItems(t *testing.T) { })) client := &http.Client{Transport: reg} - prsAndTotalCount, err := listPullRequests( + prsAndTotalCount, err := ListPullRequests( client, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{ diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 46a71a2b4af..3428986871b 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -22,6 +22,7 @@ type InputType int type Prompter interface { Select(string, string, []string) (int, error) } + const ( InputTypeEditor InputType = iota InputTypeInline From c6c27113fe4485a916299205aeaafbe16f26a8fd Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 17 Nov 2024 16:36:10 +1100 Subject: [PATCH 0005/2338] issue #2329: add happy path unit test --- pkg/cmd/pr/checkout/checkout_test.go | 52 ++++++++++++++++-- pkg/cmd/pr/checkout/fixtures/prList.json | 67 ++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/pr/checkout/fixtures/prList.json diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 1eda5dda2d1..d2962f2d9f3 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -14,6 +14,7 @@ import ( "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/internal/run" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -63,10 +64,12 @@ func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) { func Test_checkoutRun(t *testing.T) { tests := []struct { - name string - opts *CheckoutOptions - httpStubs func(*httpmock.Registry) - runStubs func(*run.CommandStubber) + name string + opts *CheckoutOptions + httpStubs func(*httpmock.Registry) + runStubs func(*run.CommandStubber) + promptStubs func(*prompter.MockPrompter) + remotes map[string]string wantStdout string wantStderr string @@ -158,6 +161,41 @@ func Test_checkoutRun(t *testing.T) { cs.Register(`git config branch\.foobar\.merge refs/heads/feature`, 0, "") }, }, + { + name: "with no selected PR args, prompts for choice", + opts: &CheckoutOptions{ + SelectorArg: "", + Finder: func() shared.PRFinder { + baseRepo, pr := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") + finder := shared.NewMockFinder("123", pr, baseRepo) + return finder + }(), + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Config: func() (gh.Config, error) { + return config.NewBlankConfig(), nil + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) + }, + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Check out a specific PR?", + []string{"32 New feature", "29 Fixed bad bug", "28 Improve documentation"}, + func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "32 New feature") + }) + }, + runStubs: func(cs *run.CommandStubber) { + cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") + cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") + cs.Register(`git checkout -b feature --track origin/feature`, 0, "") + }, + remotes: map[string]string{ + "origin": "OWNER/REPO", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -180,6 +218,12 @@ func Test_checkoutRun(t *testing.T) { tt.runStubs(cmdStubs) } + pm := prompter.NewMockPrompter(t) + tt.opts.Prompter = pm + if tt.promptStubs != nil { + tt.promptStubs(pm) + } + opts.Remotes = func() (context.Remotes, error) { if len(tt.remotes) == 0 { return nil, errors.New("no remotes") diff --git a/pkg/cmd/pr/checkout/fixtures/prList.json b/pkg/cmd/pr/checkout/fixtures/prList.json new file mode 100644 index 00000000000..f915df136f6 --- /dev/null +++ b/pkg/cmd/pr/checkout/fixtures/prList.json @@ -0,0 +1,67 @@ +{ + "data": { + "repository": { + "pullRequests": { + "totalCount": 3, + "nodes": [ + { + "number": 32, + "title": "New feature", + "url": "https://github.com/OWNER/REPO/pull/32", + "createdAt": "2022-08-24T20:01:12Z", + "headRefName": "feature", + "state": "OPEN", + "isDraft": true, + "isCrossRepository": false, + "headRepository": { + "name": "REPO" + }, + "headRepositoryOwner": { + "login": "OWNER" + }, + "maintainerCanModify": true + }, + { + "number": 29, + "title": "Fixed bad bug", + "url": "https://github.com/OWNER/REPO/pull/29", + "createdAt": "2022-07-20T19:01:12Z", + "headRefName": "bug-fix", + "state": "OPEN", + "isDraft": false, + "isCrossRepository": false, + "headRepository": { + "name": "REPO" + }, + "headRepositoryOwner": { + "login": "OWNER" + }, + "maintainerCanModify": true + }, + { + "number": 28, + "state": "MERGED", + "isDraft": false, + "title": "Improve documentation", + "createdAt": "2020-01-26T19:01:12Z", + "url": "https://github.com/OWNER/REPO/pull/28", + "isCrossRepository": false, + "headRefName": "docs", + "headRepository": { + "name": "REPO" + }, + "headRepositoryOwner": { + "login": "OWNER" + }, + "maintainerCanModify": true + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": "" + } + } + } + } + } + \ No newline at end of file From 36eaf14857dc8110c47b3d91b823589771aae305 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 17 Nov 2024 23:47:29 +1100 Subject: [PATCH 0006/2338] issue #2329: improve UI/UX Improve the interactive PR selection UI by - prefix the PR number with hashcode # - perserve the text formatting (bold) upon an option is hovered - add the PR head label Technical changes: - Replace \033[0m with \033[39m for maintaining text formatting --- pkg/cmd/pr/checkout/checkout.go | 21 ++++++++++++++++++--- pkg/cmd/pr/checkout/checkout_test.go | 4 ++-- pkg/cmd/pr/shared/display.go | 28 ++++++++++++++++++++++------ pkg/iostreams/color.go | 2 +- pkg/iostreams/color_test.go | 4 ++-- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 54c0cae93d8..cd9da809ca8 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -6,11 +6,13 @@ import ( "net/http" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" cliContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmd/pr/list" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -52,7 +54,16 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr cmd := &cobra.Command{ Use: "checkout [ | | ]", Short: "Check out a pull request in git", - Args: cobra.MaximumNArgs(1), + Example: heredoc.Doc(` + # Interactively select a PR to checkout from the 10 most recent + $ gh pr checkout + + # Checkout a specific PR + $ gh pr checkout 32 + $ gh pr checkout https://github.com/OWNER/REPO/pull/32 + $ gh pr checkout feature + `), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Finder = shared.NewFinder(f) @@ -303,7 +314,7 @@ func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter share "isCrossRepository", "isDraft", "createdAt", - }}, 30) + }}, 10) if err != nil { return nil, err } @@ -314,7 +325,11 @@ func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter share func promptForPR(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { candidates := []string{} for _, pr := range jobs.PullRequests { - candidates = append(candidates, fmt.Sprintf("%s %s", shared.PRNumberTitleWithColor(cs, pr), pr.Title)) + candidates = append(candidates, text.Truncate(120, fmt.Sprintf("%s %s (%s)", + shared.PRNumberWithColor(cs, pr), + text.RemoveExcessiveWhitespace(pr.Title), + cs.Gray(pr.HeadLabel()), + ))) } selected, err := prompter.Select("Check out a specific PR?", "", candidates) diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index d2962f2d9f3..138e1afdf90 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -182,9 +182,9 @@ func Test_checkoutRun(t *testing.T) { }, promptStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Check out a specific PR?", - []string{"32 New feature", "29 Fixed bad bug", "28 Improve documentation"}, + []string{"#32 New feature (feature)", "#29 Fixed bad bug (bug-fix)", "#28 Improve documentation (docs)"}, func(_, _ string, opts []string) (int, error) { - return prompter.IndexFor(opts, "32 New feature") + return prompter.IndexFor(opts, "#32 New feature (feature)") }) }, runStubs: func(cs *run.CommandStubber) { diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index 7920eac4e79..ffc4b4dcea7 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -17,12 +17,12 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { return prStateColorFunc(text.Title(pr.State)) } -func PRNumberTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { - prStateColorFunc := cs.ColorFromString((ColorForPRState(pr))) - prNumber := fmt.Sprintf("%d", pr.Number) - if pr.State == "OPEN" && pr.IsDraft { - return prStateColorFunc(text.Title(prNumber)) - } +// PRNumberWithColor returns a colored string representation of a pull request number +// based on its state (open, closed, merged, or draft). +// It prefixes the number with a hash symbol (#) to indicate it's a pull request. +func PRNumberWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { + prStateColorFunc := cs.ColorFromRGB(ColorHexCodeForPRState(pr)) + prNumber := fmt.Sprintf("#%d", pr.Number) return prStateColorFunc(text.Title(prNumber)) } @@ -42,6 +42,22 @@ func ColorForPRState(pr api.PullRequest) string { } } +func ColorHexCodeForPRState(pr api.PullRequest) string { + switch pr.State { + case "OPEN": + if pr.IsDraft { + return "808080" + } + return "00FF00" + case "CLOSED": + return "FF0000" + case "MERGED": + return "FFA500" + default: + return "" + } +} + func ColorForIssueState(issue api.Issue) string { switch issue.State { case "OPEN": diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index c8d48168fa4..33df6bddfe5 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -252,5 +252,5 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string { r, _ := strconv.ParseInt(hex[0:2], 16, 64) g, _ := strconv.ParseInt(hex[2:4], 16, 64) b, _ := strconv.ParseInt(hex[4:6], 16, 64) - return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x) + return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[39m", r, g, b, x) } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index 59fea53ed1e..f3370da2936 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -18,7 +18,7 @@ func TestColorFromRGB(t *testing.T) { name: "truecolor", hex: "fc0303", text: "red", - wants: "\033[38;2;252;3;3mred\033[0m", + wants: "\033[38;2;252;3;3mred\033[39m", cs: NewColorScheme(true, true, true), }, { @@ -62,7 +62,7 @@ func TestHexToRGB(t *testing.T) { name: "truecolor", hex: "fc0303", text: "red", - wants: "\033[38;2;252;3;3mred\033[0m", + wants: "\033[38;2;252;3;3mred\033[39m", cs: NewColorScheme(true, true, true), }, { From 0d3f7cae4e2a97ca633aed1d1f0404c80379f89e Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Sun, 17 Nov 2024 16:10:58 -0500 Subject: [PATCH 0007/2338] Bring extension update check in line with gh check This commit is a bit of refactoring to bring the extension update checking logic up to par with what is done with `gh` including creation of state file per extension and listening to env vars for disabling version checking. This work is not complete as it does not address necessary test changes. --- internal/ghcmd/cmd.go | 24 +------------- internal/update/update.go | 56 ++++++++++++++++++++++++++++++++ pkg/cmd/extension/extension.go | 6 +++- pkg/cmd/root/extension.go | 43 ++++++++++++------------ pkg/extensions/extension.go | 5 +-- pkg/extensions/extension_mock.go | 37 +++++++++++++++++++++ 6 files changed, 125 insertions(+), 46 deletions(-) diff --git a/internal/ghcmd/cmd.go b/internal/ghcmd/cmd.go index d5a674184be..a00dd760644 100644 --- a/internal/ghcmd/cmd.go +++ b/internal/ghcmd/cmd.go @@ -25,7 +25,6 @@ import ( "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/utils" "github.com/cli/safeexec" - "github.com/mattn/go-isatty" "github.com/mgutz/ansi" "github.com/spf13/cobra" ) @@ -216,29 +215,8 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { } } -func shouldCheckForUpdate() bool { - if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { - return false - } - if os.Getenv("CODESPACES") != "" { - return false - } - return updaterEnabled != "" && !isCI() && isTerminal(os.Stdout) && isTerminal(os.Stderr) -} - -func isTerminal(f *os.File) bool { - return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd()) -} - -// based on https://github.com/watson/ci-info/blob/HEAD/index.js -func isCI() bool { - return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari - os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity - os.Getenv("RUN_ID") != "" // TaskCluster, dsari -} - func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) { - if !shouldCheckForUpdate() { + if !update.ShouldCheckForUpdate(updaterEnabled) { return nil, nil } httpClient, err := f.HttpClient() diff --git a/internal/update/update.go b/internal/update/update.go index 6d69eeada9b..e7f05750af1 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -13,7 +13,9 @@ import ( "strings" "time" + "github.com/cli/cli/v2/pkg/extensions" "github.com/hashicorp/go-version" + "github.com/mattn/go-isatty" "gopkg.in/yaml.v3" ) @@ -31,6 +33,49 @@ type StateEntry struct { LatestRelease ReleaseInfo `yaml:"latest_release"` } +func ShouldCheckForExtensionUpdate() bool { + if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { + return false + } + if os.Getenv("CODESPACES") != "" { + return false + } + return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) +} + +func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, stateFilePath string) (*ReleaseInfo, error) { + stateEntry, _ := getStateEntry(stateFilePath) + if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { + return nil, nil + } + + releaseInfo := &ReleaseInfo{ + Version: ext.LatestVersion(), + URL: ext.URL(), + } + + err := setStateEntry(stateFilePath, time.Now(), *releaseInfo) + if err != nil { + return nil, err + } + + if ext.UpdateAvailable() { + return releaseInfo, nil + } + + return nil, nil +} + +func ShouldCheckForUpdate(updaterEnabled string) bool { + if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { + return false + } + if os.Getenv("CODESPACES") != "" { + return false + } + return updaterEnabled != "" && !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) +} + // CheckForUpdate checks whether this software has had a newer release on GitHub func CheckForUpdate(ctx context.Context, client *http.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) { stateEntry, _ := getStateEntry(stateFilePath) @@ -122,3 +167,14 @@ func versionGreaterThan(v, w string) bool { return ve == nil && we == nil && vv.GreaterThan(vw) } + +func IsTerminal(f *os.File) bool { + return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd()) +} + +// based on https://github.com/watson/ci-info/blob/HEAD/index.js +func IsCI() bool { + return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari + os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity + os.Getenv("RUN_ID") != "" // TaskCluster, dsari +} diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index dc7ffa8c3a3..5fada887c1e 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -41,7 +41,11 @@ type Extension struct { } func (e *Extension) Name() string { - return strings.TrimPrefix(filepath.Base(e.path), "gh-") + return strings.TrimPrefix(e.FullName(), "gh-") +} + +func (e *Extension) FullName() string { + return filepath.Base(e.path) } func (e *Extension) Path() string { diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index 52250a43204..a0d7dab1ec8 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -4,11 +4,14 @@ import ( "errors" "fmt" "os/exec" + "path/filepath" "strings" - "time" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/update" "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/utils" "github.com/spf13/cobra" ) @@ -16,16 +19,10 @@ type ExternalCommandExitError struct { *exec.ExitError } -type extensionReleaseInfo struct { - CurrentVersion string - LatestVersion string - Pinned bool - URL string -} - func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension) *cobra.Command { - updateMessageChan := make(chan *extensionReleaseInfo) + updateMessageChan := make(chan *update.ReleaseInfo) cs := io.ColorScheme() + hasDebug, _ := utils.IsDebugEnabled() return &cobra.Command{ Use: ext.Name(), @@ -33,14 +30,11 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex // PreRun handles looking up whether extension has a latest version only when the command is ran. PreRun: func(c *cobra.Command, args []string) { go func() { - if ext.UpdateAvailable() { - updateMessageChan <- &extensionReleaseInfo{ - CurrentVersion: ext.CurrentVersion(), - LatestVersion: ext.LatestVersion(), - Pinned: ext.IsPinned(), - URL: ext.URL(), - } + rel, err := checkForExtensionUpdate(em, ext) + if err != nil && hasDebug { + fmt.Fprintf(io.ErrOut, "warning: checking for update failed: %v", err) } + updateMessageChan <- rel }() }, RunE: func(c *cobra.Command, args []string) error { @@ -62,9 +56,9 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex 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(releaseInfo.CurrentVersion, "v")), - cs.Cyan(strings.TrimPrefix(releaseInfo.LatestVersion, "v"))) - if releaseInfo.Pinned { + 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()) @@ -72,7 +66,7 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex fmt.Fprintf(stderr, "%s\n\n", cs.Yellow(releaseInfo.URL)) } - case <-time.After(1 * time.Second): + default: // Bail on checking for new extension update as its taking too long } }, @@ -83,3 +77,12 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex DisableFlagParsing: true, } } + +func checkForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension) (*update.ReleaseInfo, error) { + if !update.ShouldCheckForExtensionUpdate() { + return nil, nil + } + + stateFilePath := filepath.Join(config.StateDir(), "extensions", ext.FullName(), "state.yml") + return update.CheckForExtensionUpdate(em, ext, stateFilePath) +} diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index 0b94032df1f..5c590314022 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -16,8 +16,9 @@ const ( //go:generate moq -rm -out extension_mock.go . Extension type Extension interface { - Name() string // Extension Name without gh- - Path() string // Path to executable + Name() string // Extension Name without gh- + FullName() string // Extension Name with gh- + Path() string // Path to executable URL() string CurrentVersion() string LatestVersion() string diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go index 9b473e20ae2..2488c44ebff 100644 --- a/pkg/extensions/extension_mock.go +++ b/pkg/extensions/extension_mock.go @@ -20,6 +20,9 @@ var _ Extension = &ExtensionMock{} // CurrentVersionFunc: func() string { // panic("mock out the CurrentVersion method") // }, +// FullNameFunc: func() string { +// panic("mock out the FullName method") +// }, // IsBinaryFunc: func() bool { // panic("mock out the IsBinary method") // }, @@ -57,6 +60,9 @@ type ExtensionMock struct { // CurrentVersionFunc mocks the CurrentVersion method. CurrentVersionFunc func() string + // FullNameFunc mocks the FullName method. + FullNameFunc func() string + // IsBinaryFunc mocks the IsBinary method. IsBinaryFunc func() bool @@ -89,6 +95,9 @@ type ExtensionMock struct { // CurrentVersion holds details about calls to the CurrentVersion method. CurrentVersion []struct { } + // FullName holds details about calls to the FullName method. + FullName []struct { + } // IsBinary holds details about calls to the IsBinary method. IsBinary []struct { } @@ -118,6 +127,7 @@ type ExtensionMock struct { } } lockCurrentVersion sync.RWMutex + lockFullName sync.RWMutex lockIsBinary sync.RWMutex lockIsLocal sync.RWMutex lockIsPinned sync.RWMutex @@ -156,6 +166,33 @@ func (mock *ExtensionMock) CurrentVersionCalls() []struct { return calls } +// FullName calls FullNameFunc. +func (mock *ExtensionMock) FullName() string { + if mock.FullNameFunc == nil { + panic("ExtensionMock.FullNameFunc: method is nil but Extension.FullName was just called") + } + callInfo := struct { + }{} + mock.lockFullName.Lock() + mock.calls.FullName = append(mock.calls.FullName, callInfo) + mock.lockFullName.Unlock() + return mock.FullNameFunc() +} + +// FullNameCalls gets all the calls that were made to FullName. +// Check the length with: +// +// len(mockedExtension.FullNameCalls()) +func (mock *ExtensionMock) FullNameCalls() []struct { +} { + var calls []struct { + } + mock.lockFullName.RLock() + calls = mock.calls.FullName + mock.lockFullName.RUnlock() + return calls +} + // IsBinary calls IsBinaryFunc. func (mock *ExtensionMock) IsBinary() bool { if mock.IsBinaryFunc == nil { From 7b91b17395a4b03723619db8fc4f02afd99677df Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Sun, 17 Nov 2024 21:59:06 -0500 Subject: [PATCH 0008/2338] Refactor extension command testing This commit allows for stubbing/mocking the function used for checking update release info. Additionally, this does a minor refactor moving from non-blocking to blocking channel. --- pkg/cmd/root/extension.go | 37 ++++++++++++++--------------- pkg/cmd/root/extension_test.go | 43 ++++++++++++++++++++++++++-------- pkg/cmd/root/root.go | 2 +- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index a0d7dab1ec8..d48087ee93a 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -19,10 +19,13 @@ type ExternalCommandExitError struct { *exec.ExitError } -func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension) *cobra.Command { +func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension, checkFunc func(extensions.ExtensionManager, extensions.Extension) (*update.ReleaseInfo, error)) *cobra.Command { updateMessageChan := make(chan *update.ReleaseInfo) cs := io.ColorScheme() hasDebug, _ := utils.IsDebugEnabled() + if checkFunc == nil { + checkFunc = checkForExtensionUpdate + } return &cobra.Command{ Use: ext.Name(), @@ -30,7 +33,7 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex // PreRun handles looking up whether extension has a latest version only when the command is ran. PreRun: func(c *cobra.Command, args []string) { go func() { - rel, err := checkForExtensionUpdate(em, ext) + rel, err := checkFunc(em, ext) if err != nil && hasDebug { fmt.Fprintf(io.ErrOut, "warning: checking for update failed: %v", err) } @@ -50,24 +53,20 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex }, // PostRun handles communicating extension release information if found PostRun: func(c *cobra.Command, args []string) { - 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)) + 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()) } - default: - // Bail on checking for new extension update as its taking too long + fmt.Fprintf(stderr, "%s\n\n", + cs.Yellow(releaseInfo.URL)) } }, GroupID: "extension", diff --git a/pkg/cmd/root/extension_test.go b/pkg/cmd/root/extension_test.go index ef94dcc715b..e71c63317fa 100644 --- a/pkg/cmd/root/extension_test.go +++ b/pkg/cmd/root/extension_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/update" "github.com/cli/cli/v2/pkg/cmd/root" "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/iostreams" @@ -16,6 +17,7 @@ func TestNewCmdExtension_Updates(t *testing.T) { tests := []struct { name string extCurrentVersion string + extFullName string extIsPinned bool extLatestVersion string extName string @@ -26,6 +28,7 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "no update available", extName: "no-update", + extFullName: "gh-no-update", extUpdateAvailable: false, extCurrentVersion: "1.0.0", extLatestVersion: "1.0.0", @@ -34,6 +37,7 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "major update", extName: "major-update", + extFullName: "gh-major-update", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "2.0.0", @@ -46,21 +50,23 @@ func TestNewCmdExtension_Updates(t *testing.T) { }, { name: "major update, pinned", - extName: "major-update", + extName: "major-update-pin", + extFullName: "gh-major-update-pin", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "2.0.0", extIsPinned: true, extURL: "https//github.com/dne/major-update", wantStderr: heredoc.Doc(` - A new release of major-update is available: 1.0.0 → 2.0.0 - To upgrade, run: gh extension upgrade major-update --force + A new release of major-update-pin is available: 1.0.0 → 2.0.0 + To upgrade, run: gh extension upgrade major-update-pin --force https//github.com/dne/major-update `), }, { name: "minor update", extName: "minor-update", + extFullName: "gh-minor-update", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.1.0", @@ -73,21 +79,23 @@ func TestNewCmdExtension_Updates(t *testing.T) { }, { name: "minor update, pinned", - extName: "minor-update", + extName: "minor-update-pin", + extFullName: "gh-minor-update-pin", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.1.0", extURL: "https//github.com/dne/minor-update", extIsPinned: true, wantStderr: heredoc.Doc(` - A new release of minor-update is available: 1.0.0 → 1.1.0 - To upgrade, run: gh extension upgrade minor-update --force + A new release of minor-update-pin is available: 1.0.0 → 1.1.0 + To upgrade, run: gh extension upgrade minor-update-pin --force https//github.com/dne/minor-update `), }, { name: "patch update", extName: "patch-update", + extFullName: "gh-patch-update", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.0.1", @@ -100,15 +108,16 @@ func TestNewCmdExtension_Updates(t *testing.T) { }, { name: "patch update, pinned", - extName: "patch-update", + extName: "patch-update-pin", + extFullName: "gh-patch-update-pin", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.0.1", extURL: "https//github.com/dne/patch-update", extIsPinned: true, wantStderr: heredoc.Doc(` - A new release of patch-update is available: 1.0.0 → 1.0.1 - To upgrade, run: gh extension upgrade patch-update --force + A new release of patch-update-pin is available: 1.0.0 → 1.0.1 + To upgrade, run: gh extension upgrade patch-update-pin --force https//github.com/dne/patch-update `), }, @@ -128,6 +137,9 @@ func TestNewCmdExtension_Updates(t *testing.T) { CurrentVersionFunc: func() string { return tt.extCurrentVersion }, + FullNameFunc: func() string { + return tt.extFullName + }, IsPinnedFunc: func() bool { return tt.extIsPinned }, @@ -145,7 +157,18 @@ func TestNewCmdExtension_Updates(t *testing.T) { }, } - cmd := root.NewCmdExtension(ios, em, ext) + checkFunc := func(em extensions.ExtensionManager, ext extensions.Extension) (*update.ReleaseInfo, error) { + if !tt.extUpdateAvailable { + return nil, nil + } + + return &update.ReleaseInfo{ + Version: tt.extLatestVersion, + URL: tt.extURL, + }, nil + } + + cmd := root.NewCmdExtension(ios, em, ext, checkFunc) _, err := cmd.ExecuteC() require.NoError(t, err) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 4b744e19273..c0dad93ec05 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -172,7 +172,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, // Extensions em := f.ExtensionManager for _, e := range em.List() { - extensionCmd := NewCmdExtension(io, em, e) + extensionCmd := NewCmdExtension(io, em, e, nil) cmd.AddCommand(extensionCmd) } From e4dced01f781659a78a20c945c36ca284eb1335b Mon Sep 17 00:00:00 2001 From: nilvng Date: Sat, 30 Nov 2024 23:51:00 +1100 Subject: [PATCH 0009/2338] issue #2329: fix missing stub --- pkg/cmd/pr/checkout/checkout_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 0d6b12bc4f5..86e7e6d70cb 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -218,6 +218,7 @@ func Test_checkoutRun(t *testing.T) { }, runStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/hubot/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git checkout -b feature --track origin/feature`, 0, "") }, From d92e529629f2ccc0c87c4a49e0b2f199602bfb3c Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 1 Dec 2024 21:13:54 +1100 Subject: [PATCH 0010/2338] issue #2329: return error when not running interactively --- pkg/cmd/pr/checkout/checkout.go | 4 ++++ pkg/cmd/pr/checkout/checkout_test.go | 23 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 54ecf17f6ef..e78eb026b0f 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -111,6 +111,10 @@ func checkoutRun(opts *CheckoutOptions) error { } default: + if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("must provide a pull request number (or URL or branch) when not running interactively") + } + httpClient, err := opts.HttpClient() if err != nil { return err diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 86e7e6d70cb..73055e1f89c 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -64,8 +64,10 @@ func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) { func Test_checkoutRun(t *testing.T) { tests := []struct { - name string - opts *CheckoutOptions + name string + opts *CheckoutOptions + tty bool + httpStubs func(*httpmock.Registry) runStubs func(*run.CommandStubber) promptStubs func(*prompter.MockPrompter) @@ -191,7 +193,15 @@ func Test_checkoutRun(t *testing.T) { }, }, { - name: "with no selected PR args, prompts for choice", + name: "with no selected PR args and not stdin tty, return error", + opts: &CheckoutOptions{ + SelectorArg: "", + }, + tty: false, + wantErr: true, + }, + { + name: "with no selected PR args and stdin tty, prompts for choice", opts: &CheckoutOptions{ SelectorArg: "", Finder: func() shared.PRFinder { @@ -206,6 +216,7 @@ func Test_checkoutRun(t *testing.T) { return config.NewBlankConfig(), nil }, }, + tty: true, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) }, @@ -232,6 +243,12 @@ func Test_checkoutRun(t *testing.T) { opts := tt.opts ios, _, stdout, stderr := iostreams.Test() + if tt.tty { + ios.SetStdinTTY(tt.tty) + ios.SetStdoutTTY(tt.tty) + ios.SetStderrTTY(tt.tty) + } + opts.IO = ios httpReg := &httpmock.Registry{} defer httpReg.Verify(t) From 6bd01d52dddc7a06a27bbc3dd1fb50f2282ad343 Mon Sep 17 00:00:00 2001 From: Andrew Feller Date: Sun, 1 Dec 2024 18:20:55 -0500 Subject: [PATCH 0011/2338] Initial extension update check testing First pass at implementing basic test around extension state checking behavior, wanting to discus with team about level of testing to perform and whether this is really the right place. --- internal/update/update.go | 10 ++- internal/update/update_test.go | 136 +++++++++++++++++++++++++++++++++ pkg/cmd/extension/extension.go | 5 ++ pkg/cmd/root/extension.go | 3 +- 4 files changed, 150 insertions(+), 4 deletions(-) diff --git a/internal/update/update.go b/internal/update/update.go index e7f05750af1..e2d72b767c3 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -43,9 +43,13 @@ func ShouldCheckForExtensionUpdate() bool { return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) } -func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, stateFilePath string) (*ReleaseInfo, error) { +func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, stateFilePath string, now time.Time) (*ReleaseInfo, error) { + if ext.IsLocal() { + return nil, nil + } + stateEntry, _ := getStateEntry(stateFilePath) - if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { + if stateEntry != nil && now.Sub(stateEntry.CheckedForUpdateAt).Hours() < 24 { return nil, nil } @@ -54,7 +58,7 @@ func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte URL: ext.URL(), } - err := setStateEntry(stateFilePath, time.Now(), *releaseInfo) + err := setStateEntry(stateFilePath, now, *releaseInfo) if err != nil { return nil, err } diff --git a/internal/update/update_test.go b/internal/update/update_test.go index bb514adfc6f..30dec50cb13 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -6,9 +6,15 @@ import ( "log" "net/http" "os" + "path/filepath" "testing" + "time" + "github.com/cli/cli/v2/pkg/cmd/extension" + "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestCheckForUpdate(t *testing.T) { @@ -117,6 +123,136 @@ func TestCheckForUpdate(t *testing.T) { } } +func TestCheckForExtensionUpdate(t *testing.T) { + now := time.Date(2024, 12, 17, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + extCurrentVersion string + extLatestVersion string + extIsLocal bool + extURL string + stateEntry *StateEntry + ri ReleaseInfo + wantErr bool + expectedReleaseInfo *ReleaseInfo + expectedStateEntry *StateEntry + }{ + { + name: "return latest release given extension is out of date and no state entry", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extIsLocal: false, + extURL: "http://example.com", + stateEntry: nil, + expectedReleaseInfo: &ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: now, + LatestRelease: ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + }, + }, + { + name: "return latest release given extension is out of date and state entry is old enough", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extIsLocal: false, + extURL: "http://example.com", + stateEntry: &StateEntry{ + CheckedForUpdateAt: now.Add(-24 * time.Hour), + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedReleaseInfo: &ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: now, + LatestRelease: ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + }, + }, + { + name: "return nothing given extension is out of date but state entry is too recent", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extIsLocal: false, + extURL: "http://example.com", + stateEntry: &StateEntry{ + CheckedForUpdateAt: now.Add(-23 * time.Hour), + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedReleaseInfo: nil, + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: now.Add(-23 * time.Hour), + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + em := &extensions.ExtensionManagerMock{} + + ext := &extensions.ExtensionMock{ + CurrentVersionFunc: func() string { + return tt.extCurrentVersion + }, + LatestVersionFunc: func() string { + return tt.extLatestVersion + }, + IsLocalFunc: func() bool { + return tt.extIsLocal + }, + URLFunc: func() string { + return tt.extURL + }, + } + + // Ensure test is testing actual update available logic + ext.UpdateAvailableFunc = func() bool { + // Should this function be removed from the extension interface? + return extension.UpdateAvailable(ext) + } + + // Create state file for test as necessary + stateFilePath := filepath.Join(t.TempDir(), "state.yml") + if tt.stateEntry != nil { + stateEntryYaml, err := yaml.Marshal(tt.stateEntry) + require.NoError(t, err) + require.NoError(t, os.WriteFile(stateFilePath, stateEntryYaml, 0644)) + } + + actual, err := CheckForExtensionUpdate(em, ext, stateFilePath, now) + if tt.wantErr { + require.Error(t, err) + } else { + require.Equal(t, tt.expectedReleaseInfo, actual) + + stateEntry, err := getStateEntry(stateFilePath) + require.NoError(t, err) + require.Equal(t, tt.expectedStateEntry, stateEntry) + } + }) + } +} + func tempFilePath() string { file, err := os.CreateTemp("", "") if err != nil { diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index 5fada887c1e..113ceab76dd 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/extensions" "gopkg.in/yaml.v3" ) @@ -215,6 +216,10 @@ func (e *Extension) Owner() string { } func (e *Extension) UpdateAvailable() bool { + return UpdateAvailable(e) +} + +func UpdateAvailable(e extensions.Extension) bool { if e.IsLocal() || e.CurrentVersion() == "" || e.LatestVersion() == "" || diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index d48087ee93a..a0b3b5755bd 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "strings" + "time" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/update" @@ -83,5 +84,5 @@ func checkForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte } stateFilePath := filepath.Join(config.StateDir(), "extensions", ext.FullName(), "state.yml") - return update.CheckForExtensionUpdate(em, ext, stateFilePath) + return update.CheckForExtensionUpdate(em, ext, stateFilePath, time.Now()) } From 985118e36ac87e22aa8967f451d6bdd6ddf19c51 Mon Sep 17 00:00:00 2001 From: nilvng Date: Tue, 3 Dec 2024 20:13:38 +1100 Subject: [PATCH 0012/2338] fix: update the copy --- pkg/cmd/pr/checkout/checkout.go | 4 ++-- pkg/cmd/pr/checkout/checkout_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index e78eb026b0f..b40aa906230 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -55,7 +55,7 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr Use: "checkout [ | | ]", Short: "Check out a pull request in git", Example: heredoc.Doc(` - # Interactively select a PR to checkout from the 10 most recent + # Interactively select a PR from the 10 most recent to check out $ gh pr checkout # Checkout a specific PR @@ -346,7 +346,7 @@ func promptForPR(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs api.P ))) } - selected, err := prompter.Select("Check out a specific PR?", "", candidates) + selected, err := prompter.Select("Select a pull request", "", candidates) if err != nil { return nil, err } diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 73055e1f89c..cf1c504d170 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -221,7 +221,7 @@ func Test_checkoutRun(t *testing.T) { reg.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) }, promptStubs: func(pm *prompter.MockPrompter) { - pm.RegisterSelect("Check out a specific PR?", + pm.RegisterSelect("Select a pull request", []string{"#32 New feature (feature)", "#29 Fixed bad bug (bug-fix)", "#28 Improve documentation (docs)"}, func(_, _ string, opts []string) (int, error) { return prompter.IndexFor(opts, "#32 New feature (feature)") From 9c82156e8626486791f9f22165f3127498c43752 Mon Sep 17 00:00:00 2001 From: nilvng Date: Tue, 3 Dec 2024 22:22:34 +1100 Subject: [PATCH 0013/2338] issue #2329: move Interactive check to RunE Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/pr/checkout/checkout.go | 7 ++++++- pkg/cmd/pr/checkout/checkout_test.go | 13 +++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index b40aa906230..9516e580a1d 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -31,6 +31,7 @@ type CheckoutOptions struct { Finder shared.PRFinder Prompter shared.Prompter + Interactive bool BaseRepo func() (ghrepo.Interface, error) SelectorArg string RecurseSubmodules bool @@ -69,6 +70,10 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr if len(args) > 0 { opts.SelectorArg = args[0] + } else if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively") + } else { + opts.Interactive = true } if runF != nil { @@ -111,7 +116,7 @@ func checkoutRun(opts *CheckoutOptions) error { } default: - if !opts.IO.CanPrompt() { + if !opts.Interactive { return cmdutil.FlagErrorf("must provide a pull request number (or URL or branch) when not running interactively") } diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index cf1c504d170..49ac0bb22a1 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -66,7 +66,6 @@ func Test_checkoutRun(t *testing.T) { tests := []struct { name string opts *CheckoutOptions - tty bool httpStubs func(*httpmock.Registry) runStubs func(*run.CommandStubber) @@ -196,14 +195,18 @@ func Test_checkoutRun(t *testing.T) { name: "with no selected PR args and not stdin tty, return error", opts: &CheckoutOptions{ SelectorArg: "", + Interactive: false, + }, + remotes: map[string]string{ + "origin": "OWNER/REPO", }, - tty: false, wantErr: true, }, { name: "with no selected PR args and stdin tty, prompts for choice", opts: &CheckoutOptions{ SelectorArg: "", + Interactive: true, Finder: func() shared.PRFinder { baseRepo, pr := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") finder := shared.NewMockFinder("123", pr, baseRepo) @@ -216,7 +219,6 @@ func Test_checkoutRun(t *testing.T) { return config.NewBlankConfig(), nil }, }, - tty: true, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) }, @@ -243,11 +245,6 @@ func Test_checkoutRun(t *testing.T) { opts := tt.opts ios, _, stdout, stderr := iostreams.Test() - if tt.tty { - ios.SetStdinTTY(tt.tty) - ios.SetStdoutTTY(tt.tty) - ios.SetStderrTTY(tt.tty) - } opts.IO = ios httpReg := &httpmock.Registry{} From 2a1f27ee01c9e9ac1f37917cc2ea9f84283fd3a8 Mon Sep 17 00:00:00 2001 From: nilvng Date: Thu, 5 Dec 2024 01:07:23 +1100 Subject: [PATCH 0014/2338] issue cli#2329: fix unsufficent properties required for PR checkout Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/pr/checkout/checkout.go | 83 ++++++++++++++-------------- pkg/cmd/pr/checkout/checkout_test.go | 48 +++++++++++----- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 9516e580a1d..b9ded029b58 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -92,47 +92,21 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr } func checkoutRun(opts *CheckoutOptions) error { - var ( - baseRepo ghrepo.Interface - pr *api.PullRequest - err error - ) - - switch { - case opts.SelectorArg != "": - pr, baseRepo, err = opts.Finder.Find(shared.FindOptions{ - Selector: opts.SelectorArg, - Fields: []string{ - "number", - "headRefName", - "headRepository", - "headRepositoryOwner", - "isCrossRepository", - "maintainerCanModify", - }, - }) - if err != nil { - return err - } - - default: - if !opts.Interactive { - return cmdutil.FlagErrorf("must provide a pull request number (or URL or branch) when not running interactively") - } - - httpClient, err := opts.HttpClient() - if err != nil { - return err - } + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } - if baseRepo, err = opts.BaseRepo(); err != nil { - return err - } + client, err := opts.HttpClient() + if err != nil { + return err + } - if pr, err = selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()); err != nil { - return err - } + pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.IO.ColorScheme()) + if err != nil { + return err } + cfg, err := opts.Config() if err != nil { return err @@ -322,22 +296,49 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm return nil } -func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, cs *iostreams.ColorScheme) (*api.PullRequest, error) { +func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, cs *iostreams.ColorScheme) (*api.PullRequest, error) { + // When non-interactive + if pullRequestSelector != "" { + pr, _, err := pullRequestFinder.Find(shared.FindOptions{ + Selector: pullRequestSelector, + Fields: []string{ + "number", + "headRefName", + "headRepository", + "headRepositoryOwner", + "isCrossRepository", + "maintainerCanModify", + }, + }) + if err != nil { + return nil, err + } + return pr, nil + } + if !isInteractive { + return nil, cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively") + } + // When interactive listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{ "number", "title", "state", "url", + "isDraft", + "createdAt", + "headRefName", + "headRepository", "headRepositoryOwner", "isCrossRepository", - "isDraft", - "createdAt", + "maintainerCanModify", }}, 10) if err != nil { return nil, err } + pr, err := promptForPR(prompter, cs, *listResult) + return pr, err } diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 49ac0bb22a1..5c2109f20f5 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -85,6 +85,10 @@ func Test_checkoutRun(t *testing.T) { finder := shared.NewMockFinder("123", pr, baseRepo) return finder }(), + BaseRepo: func() (ghrepo.Interface, error) { + baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") + return baseRepo, nil + }, Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, @@ -113,6 +117,10 @@ func Test_checkoutRun(t *testing.T) { finder := shared.NewMockFinder("123", pr, baseRepo) return finder }(), + BaseRepo: func() (ghrepo.Interface, error) { + baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") + return baseRepo, nil + }, Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, @@ -143,6 +151,10 @@ func Test_checkoutRun(t *testing.T) { finder := shared.NewMockFinder("123", pr, baseRepo) return finder }(), + BaseRepo: func() (ghrepo.Interface, error) { + baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") + return baseRepo, nil + }, Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, @@ -171,6 +183,10 @@ func Test_checkoutRun(t *testing.T) { finder := shared.NewMockFinder("123", pr, baseRepo) return finder }(), + BaseRepo: func() (ghrepo.Interface, error) { + baseRepo, _ := stubPR("OWNER/REPO:master", "hubot/REPO:feature") + return baseRepo, nil + }, Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, @@ -196,6 +212,9 @@ func Test_checkoutRun(t *testing.T) { opts: &CheckoutOptions{ SelectorArg: "", Interactive: false, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, }, remotes: map[string]string{ "origin": "OWNER/REPO", @@ -303,7 +322,7 @@ func Test_checkoutRun(t *testing.T) { /** LEGACY TESTS **/ -func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string) (*test.CmdOut, error) { +func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string, baseRepo ghrepo.Interface) (*test.CmdOut, error) { ios, _, stdout, stderr := iostreams.Test() factory := &cmdutil.Factory{ @@ -332,6 +351,9 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cl GhPath: "some/path/gh", GitPath: "some/path/git", }, + BaseRepo: func() (ghrepo.Interface, error) { + return baseRepo, nil + }, } cmd := NewCmdCheckout(factory, nil) @@ -369,7 +391,7 @@ func TestPRCheckout_sameRepo(t *testing.T) { cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") cs.Register(`git checkout -b feature --track origin/feature`, 0, "") - output, err := runCommand(http, nil, "master", `123`) + output, err := runCommand(http, nil, "master", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -391,7 +413,7 @@ func TestPRCheckout_existingBranch(t *testing.T) { cs.Register(`git checkout feature`, 0, "") cs.Register(`git merge --ff-only refs/remotes/origin/feature`, 0, "") - output, err := runCommand(http, nil, "master", `123`) + output, err := runCommand(http, nil, "master", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -424,7 +446,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) { cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") cs.Register(`git checkout -b feature --track robot-fork/feature`, 0, "") - output, err := runCommand(http, remotes, "master", `123`) + output, err := runCommand(http, remotes, "master", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -449,7 +471,7 @@ func TestPRCheckout_differentRepo(t *testing.T) { cs.Register(`git config branch\.feature\.pushRemote origin`, 0, "") cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "") - output, err := runCommand(http, nil, "master", `123`) + output, err := runCommand(http, nil, "master", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -470,7 +492,7 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) { cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n") cs.Register(`git checkout feature`, 0, "") - output, err := runCommand(http, nil, "master", `123`) + output, err := runCommand(http, nil, "master", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -491,7 +513,7 @@ func TestPRCheckout_detachedHead(t *testing.T) { cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n") cs.Register(`git checkout feature`, 0, "") - output, err := runCommand(http, nil, "", `123`) + output, err := runCommand(http, nil, "", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -512,7 +534,7 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) { cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n") cs.Register(`git merge --ff-only FETCH_HEAD`, 0, "") - output, err := runCommand(http, nil, "feature", `123`) + output, err := runCommand(http, nil, "feature", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -528,7 +550,7 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) { _, cmdTeardown := run.Stub() defer cmdTeardown(t) - output, err := runCommand(http, nil, "master", `123`) + output, err := runCommand(http, nil, "master", `123`, baseRepo) assert.EqualError(t, err, `invalid branch name: "-foo"`) assert.Equal(t, "", output.Stderr()) assert.Equal(t, "", output.Stderr()) @@ -553,7 +575,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) { cs.Register(`git config branch\.feature\.pushRemote https://github\.com/hubot/REPO\.git`, 0, "") cs.Register(`git config branch\.feature\.merge refs/heads/feature`, 0, "") - output, err := runCommand(http, nil, "master", `123`) + output, err := runCommand(http, nil, "master", `123`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -576,7 +598,7 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) { cs.Register(`git submodule sync --recursive`, 0, "") cs.Register(`git submodule update --init --recursive`, 0, "") - output, err := runCommand(http, nil, "master", `123 --recurse-submodules`) + output, err := runCommand(http, nil, "master", `123 --recurse-submodules`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) @@ -597,7 +619,7 @@ func TestPRCheckout_force(t *testing.T) { cs.Register(`git checkout feature`, 0, "") cs.Register(`git reset --hard refs/remotes/origin/feature`, 0, "") - output, err := runCommand(http, nil, "master", `123 --force`) + output, err := runCommand(http, nil, "master", `123 --force`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) @@ -618,7 +640,7 @@ func TestPRCheckout_detach(t *testing.T) { cs.Register(`git remote get-url origin`, 0, "https://github.com/hubot/REPO.git") cs.Register(`git fetch origin refs/pull/123/head`, 0, "") - output, err := runCommand(http, nil, "", `123 --detach`) + output, err := runCommand(http, nil, "", `123 --detach`, baseRepo) assert.NoError(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) From 490bfdac0339c16049f8e37ce5a434d0a331ea7a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:40:28 -0700 Subject: [PATCH 0015/2338] issue cli#2329: Fix linter --- pkg/cmd/pr/checkout/checkout_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 428125bbb5c..fe68fe059f0 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -532,9 +532,9 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) { _, cmdTeardown := run.Stub() defer cmdTeardown(t) - + output, err := runCommand(http, nil, "master", `123`, baseRepo) - + assert.EqualError(t, err, `invalid branch name: "-foo"`) assert.Equal(t, "", output.Stderr()) assert.Equal(t, "", output.Stderr()) From 12479e307282fabc9c6d68981073477de6a5c07c Mon Sep 17 00:00:00 2001 From: nilvng Date: Fri, 6 Dec 2024 18:19:07 +1100 Subject: [PATCH 0016/2338] issue #2329: fix unit tests --- pkg/cmd/pr/checkout/checkout_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index fe68fe059f0..4bbf23cdb1e 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -246,7 +246,6 @@ func Test_checkoutRun(t *testing.T) { }, runStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/hubot/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git checkout -b feature --track origin/feature`, 0, "") }, From eced54c9044e78deeb202722c96e2b15d178f12d Mon Sep 17 00:00:00 2001 From: nilvng Date: Fri, 6 Dec 2024 18:49:43 +1100 Subject: [PATCH 0017/2338] issue #2329: update the prompt's format Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/pr/checkout/checkout.go | 2 +- pkg/cmd/pr/checkout/checkout_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 8fb591b5823..adaa6abe6f5 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -340,7 +340,7 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar func promptForPR(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { candidates := []string{} for _, pr := range jobs.PullRequests { - candidates = append(candidates, text.Truncate(120, fmt.Sprintf("%s %s (%s)", + candidates = append(candidates, text.Truncate(120, fmt.Sprintf("%s %s [%s]", shared.PRNumberWithColor(cs, pr), text.RemoveExcessiveWhitespace(pr.Title), cs.Gray(pr.HeadLabel()), diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 4bbf23cdb1e..33173252e7b 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -239,9 +239,9 @@ func Test_checkoutRun(t *testing.T) { }, promptStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Select a pull request", - []string{"#32 New feature (feature)", "#29 Fixed bad bug (bug-fix)", "#28 Improve documentation (docs)"}, + []string{"#32 New feature [feature]", "#29 Fixed bad bug [bug-fix]", "#28 Improve documentation [docs]"}, func(_, _ string, opts []string) (int, error) { - return prompter.IndexFor(opts, "#32 New feature (feature)") + return prompter.IndexFor(opts, "#32 New feature [feature]") }) }, runStubs: func(cs *run.CommandStubber) { From 409e3ca08caffe21b4e3c6a09915ffc55eea5b49 Mon Sep 17 00:00:00 2001 From: nilvng Date: Fri, 6 Dec 2024 19:16:00 +1100 Subject: [PATCH 0018/2338] issue #2329: simplify the UI of the prompt This commit reverts the previous color changes in the prompt UI. While color highlighting could potentially improve the visual appealing of the prompt using the existing color library (mguz/ansi) with the prompt library (AlecAivazis/survey) caused unintended side effects. It reset the bold text style for the selected option. We decide to that bold text style would have a higher priority than the color text ,for three reasons: 1. To maintain consistency with other prompts in the UI and prioritize accessibility 2. While color can enhance the user experience, according to Primer Design Guidelines, color should not be relied upon to convey essential information. 3. visual indicator of the selected option, especially crucial when dealing with long PR titles or branch names. As a future improvement, we may consider a separate issue or PR to address the color library issue and explore controlled color usage in prompts. This could potentially allow for more nuanced visual differentiation while avoiding unintended style resets. Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/pr/checkout/checkout.go | 16 ++++++++-------- pkg/cmd/pr/shared/display.go | 25 ------------------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index adaa6abe6f5..e60658955b8 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -102,7 +102,7 @@ func checkoutRun(opts *CheckoutOptions) error { return err } - pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.IO.ColorScheme()) + pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder) if err != nil { return err } @@ -291,7 +291,7 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm return nil } -func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, cs *iostreams.ColorScheme) (*api.PullRequest, error) { +func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder) (*api.PullRequest, error) { // When non-interactive if pullRequestSelector != "" { pr, _, err := pullRequestFinder.Find(shared.FindOptions{ @@ -332,19 +332,19 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar return nil, err } - pr, err := promptForPR(prompter, cs, *listResult) + pr, err := promptForPR(prompter, *listResult) return pr, err } -func promptForPR(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { +func promptForPR(prompter shared.Prompter, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { candidates := []string{} for _, pr := range jobs.PullRequests { - candidates = append(candidates, text.Truncate(120, fmt.Sprintf("%s %s [%s]", - shared.PRNumberWithColor(cs, pr), + candidates = append(candidates, fmt.Sprintf("#%d %s [%s]", + pr.Number, text.RemoveExcessiveWhitespace(pr.Title), - cs.Gray(pr.HeadLabel()), - ))) + pr.HeadLabel(), + )) } selected, err := prompter.Select("Select a pull request", "", candidates) diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index ffc4b4dcea7..7bd306104c0 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -17,15 +17,6 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { return prStateColorFunc(text.Title(pr.State)) } -// PRNumberWithColor returns a colored string representation of a pull request number -// based on its state (open, closed, merged, or draft). -// It prefixes the number with a hash symbol (#) to indicate it's a pull request. -func PRNumberWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { - prStateColorFunc := cs.ColorFromRGB(ColorHexCodeForPRState(pr)) - prNumber := fmt.Sprintf("#%d", pr.Number) - return prStateColorFunc(text.Title(prNumber)) -} - func ColorForPRState(pr api.PullRequest) string { switch pr.State { case "OPEN": @@ -42,22 +33,6 @@ func ColorForPRState(pr api.PullRequest) string { } } -func ColorHexCodeForPRState(pr api.PullRequest) string { - switch pr.State { - case "OPEN": - if pr.IsDraft { - return "808080" - } - return "00FF00" - case "CLOSED": - return "FF0000" - case "MERGED": - return "FFA500" - default: - return "" - } -} - func ColorForIssueState(issue api.Issue) string { switch issue.State { case "OPEN": From 97630fe73c1c902fba9db492aff8dedaa733875e Mon Sep 17 00:00:00 2001 From: Andrew Feller Date: Sun, 8 Dec 2024 19:40:24 -0500 Subject: [PATCH 0019/2338] Enhance extension manager and tests - extension manager has been made responsible for extension update directory information - extension manager has been enhanced to ensure that previously left extension update metadata is deleted before installing an extension - extension manager has been enhanced to ensure that extension update metadata is deleted when extension is being removed - refactored extension command tests for manager change, ensuring previous and expected states along with returned release info - refactored extension manager tests for ensuring previous extension update entries are removed before installing extension - created extension manager test for installing local extension - centralized logic for checking and ensuring extension name is "gh-" prefixed --- internal/update/update.go | 3 +- internal/update/update_test.go | 78 ++++--- pkg/cmd/extension/extension.go | 5 - pkg/cmd/extension/manager.go | 57 ++++- pkg/cmd/extension/manager_test.go | 362 ++++++++++++++++++++---------- pkg/cmd/root/extension.go | 5 +- pkg/extensions/extension.go | 1 + pkg/extensions/manager_mock.go | 44 ++++ 8 files changed, 387 insertions(+), 168 deletions(-) diff --git a/internal/update/update.go b/internal/update/update.go index e2d72b767c3..6010689c7aa 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -43,11 +43,12 @@ func ShouldCheckForExtensionUpdate() bool { return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) } -func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, stateFilePath string, now time.Time) (*ReleaseInfo, error) { +func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, now time.Time) (*ReleaseInfo, error) { if ext.IsLocal() { return nil, nil } + stateFilePath := filepath.Join(em.UpdateDir(ext.FullName()), "state.yml") // TODO: See what removing FullName() changes stateEntry, _ := getStateEntry(stateFilePath) if stateEntry != nil && now.Sub(stateEntry.CheckedForUpdateAt).Hours() < 24 { return nil, nil diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 30dec50cb13..6f7db6e701d 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -10,11 +10,9 @@ import ( "testing" "time" - "github.com/cli/cli/v2/pkg/cmd/extension" "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) func TestCheckForUpdate(t *testing.T) { @@ -125,6 +123,8 @@ func TestCheckForUpdate(t *testing.T) { func TestCheckForExtensionUpdate(t *testing.T) { now := time.Date(2024, 12, 17, 12, 0, 0, 0, time.UTC) + previousTooSoon := now.Add(-23 * time.Hour).Add(-59 * time.Minute).Add(-59 * time.Second) + previousOldEnough := now.Add(-24 * time.Hour) tests := []struct { name string @@ -132,11 +132,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { extLatestVersion string extIsLocal bool extURL string - stateEntry *StateEntry - ri ReleaseInfo - wantErr bool - expectedReleaseInfo *ReleaseInfo + previousStateEntry *StateEntry expectedStateEntry *StateEntry + expectedReleaseInfo *ReleaseInfo + wantErr bool }{ { name: "return latest release given extension is out of date and no state entry", @@ -144,11 +143,6 @@ func TestCheckForExtensionUpdate(t *testing.T) { extLatestVersion: "v1.0.0", extIsLocal: false, extURL: "http://example.com", - stateEntry: nil, - expectedReleaseInfo: &ReleaseInfo{ - Version: "v1.0.0", - URL: "http://example.com", - }, expectedStateEntry: &StateEntry{ CheckedForUpdateAt: now, LatestRelease: ReleaseInfo{ @@ -156,6 +150,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { URL: "http://example.com", }, }, + expectedReleaseInfo: &ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, }, { name: "return latest release given extension is out of date and state entry is old enough", @@ -163,17 +161,13 @@ func TestCheckForExtensionUpdate(t *testing.T) { extLatestVersion: "v1.0.0", extIsLocal: false, extURL: "http://example.com", - stateEntry: &StateEntry{ - CheckedForUpdateAt: now.Add(-24 * time.Hour), + previousStateEntry: &StateEntry{ + CheckedForUpdateAt: previousOldEnough, LatestRelease: ReleaseInfo{ Version: "v0.1.0", URL: "http://example.com", }, }, - expectedReleaseInfo: &ReleaseInfo{ - Version: "v1.0.0", - URL: "http://example.com", - }, expectedStateEntry: &StateEntry{ CheckedForUpdateAt: now, LatestRelease: ReleaseInfo{ @@ -181,6 +175,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { URL: "http://example.com", }, }, + expectedReleaseInfo: &ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, }, { name: "return nothing given extension is out of date but state entry is too recent", @@ -188,29 +186,41 @@ func TestCheckForExtensionUpdate(t *testing.T) { extLatestVersion: "v1.0.0", extIsLocal: false, extURL: "http://example.com", - stateEntry: &StateEntry{ - CheckedForUpdateAt: now.Add(-23 * time.Hour), + previousStateEntry: &StateEntry{ + CheckedForUpdateAt: previousTooSoon, LatestRelease: ReleaseInfo{ Version: "v0.1.0", URL: "http://example.com", }, }, - expectedReleaseInfo: nil, expectedStateEntry: &StateEntry{ - CheckedForUpdateAt: now.Add(-23 * time.Hour), + CheckedForUpdateAt: previousTooSoon, LatestRelease: ReleaseInfo{ Version: "v0.1.0", URL: "http://example.com", }, }, + expectedReleaseInfo: nil, }, + // TODO: Local extension with no previous state entry } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - em := &extensions.ExtensionManagerMock{} + updateDir := t.TempDir() + em := &extensions.ExtensionManagerMock{ + UpdateDirFunc: func(name string) string { + return filepath.Join(updateDir, name) + }, + } ext := &extensions.ExtensionMock{ + NameFunc: func() string { + return "extension-update-test" + }, + FullNameFunc: func() string { + return "gh-extension-update-test" + }, CurrentVersionFunc: func() string { return tt.extCurrentVersion }, @@ -225,21 +235,25 @@ func TestCheckForExtensionUpdate(t *testing.T) { }, } - // Ensure test is testing actual update available logic + // UpdateAvailable is arguably code under test but moq does not support partial mocks so this is a little brittle. ext.UpdateAvailableFunc = func() bool { - // Should this function be removed from the extension interface? - return extension.UpdateAvailable(ext) + if ext.IsLocal() { + panic("Local extensions do not get update notices") + } + + // Actual extension versions should drive tests instead of managing UpdateAvailable separately. + current := ext.CurrentVersion() + latest := ext.LatestVersion() + return current != "" && latest != "" && current != latest } - // Create state file for test as necessary - stateFilePath := filepath.Join(t.TempDir(), "state.yml") - if tt.stateEntry != nil { - stateEntryYaml, err := yaml.Marshal(tt.stateEntry) - require.NoError(t, err) - require.NoError(t, os.WriteFile(stateFilePath, stateEntryYaml, 0644)) + // Setup previous state file for test as necessary + stateFilePath := filepath.Join(em.UpdateDir(ext.FullName()), "state.yml") + if tt.previousStateEntry != nil { + require.NoError(t, setStateEntry(stateFilePath, tt.previousStateEntry.CheckedForUpdateAt, tt.previousStateEntry.LatestRelease)) } - actual, err := CheckForExtensionUpdate(em, ext, stateFilePath, now) + actual, err := CheckForExtensionUpdate(em, ext, now) if tt.wantErr { require.Error(t, err) } else { diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index 113ceab76dd..5fada887c1e 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -11,7 +11,6 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/extensions" "gopkg.in/yaml.v3" ) @@ -216,10 +215,6 @@ func (e *Extension) Owner() string { } func (e *Extension) UpdateAvailable() bool { - return UpdateAvailable(e) -} - -func UpdateAvailable(e extensions.Extension) bool { if e.IsLocal() || e.CurrentVersion() == "" || e.LatestVersion() == "" || diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 2431c6b83e5..4670e120f85 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -33,6 +33,7 @@ const darwinAmd64 = "darwin-amd64" type Manager struct { dataDir func() string + updateDir func() string lookPath func(string) (string, error) findSh func() (string, error) newCommand func(string, ...string) *exec.Cmd @@ -46,7 +47,10 @@ type Manager struct { func NewManager(ios *iostreams.IOStreams, gc *git.Client) *Manager { return &Manager{ - dataDir: config.DataDir, + dataDir: config.DataDir, + updateDir: func() string { + return filepath.Join(config.StateDir(), "extensions") + }, lookPath: safeexec.LookPath, findSh: findsh.Find, newCommand: exec.Command, @@ -193,6 +197,9 @@ func (m *Manager) populateLatestVersions(exts []*Extension) { func (m *Manager) InstallLocal(dir string) error { name := filepath.Base(dir) + if err := m.cleanExtensionUpdateDir(name); err != nil { + return err + } targetLink := filepath.Join(m.installDir(), name) if err := os.MkdirAll(filepath.Dir(targetLink), 0755); err != nil { return err @@ -299,8 +306,11 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { // TODO clean this up if function errs? if !m.dryRunMode { - err = os.MkdirAll(targetDir, 0755) - if err != nil { + if err := m.cleanExtensionUpdateDir(name); err != nil { + return err + } + + if err = os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("failed to create installation directory: %w", err) } } @@ -386,6 +396,10 @@ func (m *Manager) installGit(repo ghrepo.Interface, target string) error { name := strings.TrimSuffix(path.Base(cloneURL), ".git") targetDir := filepath.Join(m.installDir(), name) + if err := m.cleanExtensionUpdateDir(name); err != nil { + return err + } + _, err := m.gitClient.Clone(cloneURL, []string{targetDir}) if err != nil { return err @@ -528,13 +542,20 @@ func (m *Manager) upgradeBinExtension(ext *Extension) error { } func (m *Manager) Remove(name string) error { - targetDir := filepath.Join(m.installDir(), "gh-"+name) + name, err := ensurePrefixed(name) + if err != nil { + return err + } + targetDir := filepath.Join(m.installDir(), name) if _, err := os.Lstat(targetDir); os.IsNotExist(err) { return fmt.Errorf("no extension found: %q", targetDir) } if m.dryRunMode { return nil } + if err := m.cleanExtensionUpdateDir(name); err != nil { + return err + } return os.RemoveAll(targetDir) } @@ -542,6 +563,11 @@ func (m *Manager) installDir() string { return filepath.Join(m.dataDir(), "extensions") } +// UpdateDir returns the extension-specific directory where updates are stored. +func (m *Manager) UpdateDir(name string) string { + return filepath.Join(m.updateDir(), name) +} + //go:embed ext_tmpls/goBinMain.go.txt var mainGoTmpl string @@ -802,3 +828,26 @@ func codesignBinary(binPath string) error { cmd := exec.Command(codesignExe, "--sign", "-", "--force", "--preserve-metadata=entitlements,requirements,flags,runtime", binPath) return cmd.Run() } + +// cleanExtensionUpdateDir should be used before installing extensions to avoid past metadata creating problems. +func (m *Manager) cleanExtensionUpdateDir(name string) error { + updatePath := m.UpdateDir(name) + if _, err := os.Stat(updatePath); err == nil { + if err := os.RemoveAll(updatePath); err != nil { + return fmt.Errorf("failed to remove previous extension update state: %w", err) + } + } + + return nil +} + +// ensurePrefixed just makes sure that the provided extension name is prefixed with "gh-". +func ensurePrefixed(name string) (string, error) { + if name == "" { + return "", errors.New("failed prefixing extension name: must not be empty") + } + if !strings.HasPrefix(name, "gh-") { + name = "gh-" + name + } + return name, nil +} diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index e25bc1496ec..9564fc6fa43 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -38,11 +38,12 @@ func TestHelperProcess(t *testing.T) { os.Exit(0) } -func newTestManager(dir string, client *http.Client, gitClient gitClient, ios *iostreams.IOStreams) *Manager { +func newTestManager(dataDir, updateDir string, client *http.Client, gitClient gitClient, ios *iostreams.IOStreams) *Manager { return &Manager{ - dataDir: func() string { return dir }, - lookPath: func(exe string) (string, error) { return exe, nil }, - findSh: func() (string, error) { return "sh", nil }, + dataDir: func() string { return dataDir }, + updateDir: func() string { return updateDir }, + lookPath: func(exe string) (string, error) { return exe, nil }, + findSh: func() (string, error) { return "sh", nil }, newCommand: func(exe string, args ...string) *exec.Cmd { args = append([]string{os.Args[0], "-test.run=TestHelperProcess", "--", exe}, args...) cmd := exec.Command(args[0], args[1:]...) @@ -64,12 +65,13 @@ func newTestManager(dir string, client *http.Client, gitClient gitClient, ios *i } func TestManager_List(t *testing.T) { - tempDir := t.TempDir() - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-two", "gh-two"))) assert.NoError(t, stubBinaryExtension( - filepath.Join(tempDir, "extensions", "gh-bin-ext"), + filepath.Join(dataDir, "extensions", "gh-bin-ext"), binManifest{ Owner: "owner", Name: "gh-bin-ext", @@ -77,13 +79,13 @@ func TestManager_List(t *testing.T) { Tag: "v1.0.1", })) - dirOne := filepath.Join(tempDir, "extensions", "gh-hello") - dirTwo := filepath.Join(tempDir, "extensions", "gh-two") + dirOne := filepath.Join(dataDir, "extensions", "gh-hello") + dirTwo := filepath.Join(dataDir, "extensions", "gh-two") gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", dirOne).Return(gcOne).Once() gc.On("ForRepo", dirTwo).Return(gcTwo).Once() - m := newTestManager(tempDir, nil, gc, nil) + m := newTestManager(dataDir, updateDir, nil, gc, nil) exts := m.List() assert.Equal(t, 3, len(exts)) @@ -96,10 +98,11 @@ func TestManager_List(t *testing.T) { } func TestManager_list_includeMetadata(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() assert.NoError(t, stubBinaryExtension( - filepath.Join(tempDir, "extensions", "gh-bin-ext"), + filepath.Join(dataDir, "extensions", "gh-bin-ext"), binManifest{ Owner: "owner", Name: "gh-bin-ext", @@ -124,7 +127,7 @@ func TestManager_list_includeMetadata(t *testing.T) { }, })) - m := newTestManager(tempDir, &client, nil, nil) + m := newTestManager(dataDir, updateDir, &client, nil, nil) exts, err := m.list(true) assert.NoError(t, err) @@ -135,15 +138,16 @@ func TestManager_list_includeMetadata(t *testing.T) { } func TestManager_Dispatch(t *testing.T) { - tempDir := t.TempDir() - extDir := filepath.Join(tempDir, "extensions", "gh-hello") - extPath := filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello") + dataDir := t.TempDir() + updateDir := t.TempDir() + extDir := filepath.Join(dataDir, "extensions", "gh-hello") + extPath := filepath.Join(dataDir, "extensions", "gh-hello", "gh-hello") assert.NoError(t, stubExtension(extPath)) gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extDir).Return(gcOne).Once() - m := newTestManager(tempDir, nil, gc, nil) + m := newTestManager(dataDir, updateDir, nil, gc, nil) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} @@ -163,8 +167,9 @@ func TestManager_Dispatch(t *testing.T) { } func TestManager_Dispatch_binary(t *testing.T) { - tempDir := t.TempDir() - extPath := filepath.Join(tempDir, "extensions", "gh-hello") + dataDir := t.TempDir() + updateDir := t.TempDir() + extPath := filepath.Join(dataDir, "extensions", "gh-hello") exePath := filepath.Join(extPath, "gh-hello") bm := binManifest{ Owner: "owner", @@ -174,7 +179,7 @@ func TestManager_Dispatch_binary(t *testing.T) { } assert.NoError(t, stubBinaryExtension(extPath, bm)) - m := newTestManager(tempDir, nil, nil, nil) + m := newTestManager(dataDir, updateDir, nil, nil, nil) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} @@ -187,24 +192,30 @@ func TestManager_Dispatch_binary(t *testing.T) { } func TestManager_Remove(t *testing.T) { - tempDir := t.TempDir() - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-two", "gh-two"))) - m := newTestManager(tempDir, nil, nil, nil) + m := newTestManager(dataDir, updateDir, nil, nil, nil) err := m.Remove("hello") assert.NoError(t, err) - items, err := os.ReadDir(filepath.Join(tempDir, "extensions")) + items, err := os.ReadDir(filepath.Join(dataDir, "extensions")) assert.NoError(t, err) assert.Equal(t, 1, len(items)) assert.Equal(t, "gh-two", items[0].Name()) + + assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-hello"), "update directory should be removed") } func TestManager_Upgrade_NoExtensions(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, nil, nil, ios) + + m := newTestManager(dataDir, updateDir, nil, nil, ios) err := m.Upgrade("", false) assert.EqualError(t, err, "no extensions installed") assert.Equal(t, "", stdout.String()) @@ -212,13 +223,15 @@ func TestManager_Upgrade_NoExtensions(t *testing.T) { } func TestManager_Upgrade_NoMatchingExtension(t *testing.T) { - tempDir := t.TempDir() - extDir := filepath.Join(tempDir, "extensions", "gh-hello") - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + extDir := filepath.Join(dataDir, "extensions", "gh-hello") + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-hello", "gh-hello"))) ios, _, stdout, stderr := iostreams.Test() gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extDir).Return(gcOne).Once() - m := newTestManager(tempDir, nil, gc, ios) + + m := newTestManager(dataDir, updateDir, nil, gc, ios) err := m.Upgrade("invalid", false) assert.EqualError(t, err, `no extension matched "invalid"`) assert.Equal(t, "", stdout.String()) @@ -228,12 +241,13 @@ func TestManager_Upgrade_NoMatchingExtension(t *testing.T) { } func TestManager_UpgradeExtensions(t *testing.T) { - tempDir := t.TempDir() - dirOne := filepath.Join(tempDir, "extensions", "gh-hello") - dirTwo := filepath.Join(tempDir, "extensions", "gh-two") - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) - assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + dirOne := filepath.Join(dataDir, "extensions", "gh-hello") + dirTwo := filepath.Join(dataDir, "extensions", "gh-two") + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubLocalExtension(dataDir, filepath.Join(dataDir, "extensions", "gh-local", "gh-local"))) ios, _, stdout, stderr := iostreams.Test() gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", dirOne).Return(gcOne).Times(3) @@ -242,7 +256,8 @@ func TestManager_UpgradeExtensions(t *testing.T) { gcTwo.On("Remotes").Return(nil, nil).Once() gcOne.On("Pull", "", "").Return(nil).Once() gcTwo.On("Pull", "", "").Return(nil).Once() - m := newTestManager(tempDir, nil, gc, ios) + + m := newTestManager(dataDir, updateDir, nil, gc, ios) exts, err := m.list(false) assert.NoError(t, err) assert.Equal(t, 3, len(exts)) @@ -266,19 +281,21 @@ func TestManager_UpgradeExtensions(t *testing.T) { } func TestManager_UpgradeExtensions_DryRun(t *testing.T) { - tempDir := t.TempDir() - dirOne := filepath.Join(tempDir, "extensions", "gh-hello") - dirTwo := filepath.Join(tempDir, "extensions", "gh-two") - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) - assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + dirOne := filepath.Join(dataDir, "extensions", "gh-hello") + dirTwo := filepath.Join(dataDir, "extensions", "gh-two") + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubLocalExtension(dataDir, filepath.Join(dataDir, "extensions", "gh-local", "gh-local"))) ios, _, stdout, stderr := iostreams.Test() gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", dirOne).Return(gcOne).Twice() gc.On("ForRepo", dirTwo).Return(gcTwo).Twice() gcOne.On("Remotes").Return(nil, nil).Once() gcTwo.On("Remotes").Return(nil, nil).Once() - m := newTestManager(tempDir, nil, gc, ios) + + m := newTestManager(dataDir, updateDir, nil, gc, ios) m.EnableDryRunMode() exts, err := m.list(false) assert.NoError(t, err) @@ -303,11 +320,12 @@ func TestManager_UpgradeExtensions_DryRun(t *testing.T) { } func TestManager_UpgradeExtension_LocalExtension(t *testing.T) { - tempDir := t.TempDir() - assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + assert.NoError(t, stubLocalExtension(dataDir, filepath.Join(dataDir, "extensions", "gh-local", "gh-local"))) ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, nil, nil, ios) + m := newTestManager(dataDir, updateDir, nil, nil, ios) exts, err := m.list(false) assert.NoError(t, err) assert.Equal(t, 1, len(exts)) @@ -318,11 +336,12 @@ func TestManager_UpgradeExtension_LocalExtension(t *testing.T) { } func TestManager_UpgradeExtension_LocalExtension_DryRun(t *testing.T) { - tempDir := t.TempDir() - assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + assert.NoError(t, stubLocalExtension(dataDir, filepath.Join(dataDir, "extensions", "gh-local", "gh-local"))) ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, nil, nil, ios) + m := newTestManager(dataDir, updateDir, nil, nil, ios) m.EnableDryRunMode() exts, err := m.list(false) assert.NoError(t, err) @@ -334,15 +353,17 @@ func TestManager_UpgradeExtension_LocalExtension_DryRun(t *testing.T) { } func TestManager_UpgradeExtension_GitExtension(t *testing.T) { - tempDir := t.TempDir() - extensionDir := filepath.Join(tempDir, "extensions", "gh-remote") - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + extensionDir := filepath.Join(dataDir, "extensions", "gh-remote") + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-remote", "gh-remote"))) ios, _, stdout, stderr := iostreams.Test() gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extensionDir).Return(gcOne).Times(3) gcOne.On("Remotes").Return(nil, nil).Once() gcOne.On("Pull", "", "").Return(nil).Once() - m := newTestManager(tempDir, nil, gc, ios) + + m := newTestManager(dataDir, updateDir, nil, gc, ios) exts, err := m.list(false) assert.NoError(t, err) assert.Equal(t, 1, len(exts)) @@ -358,14 +379,16 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) { } func TestManager_UpgradeExtension_GitExtension_DryRun(t *testing.T) { - tempDir := t.TempDir() - extDir := filepath.Join(tempDir, "extensions", "gh-remote") - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + extDir := filepath.Join(dataDir, "extensions", "gh-remote") + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-remote", "gh-remote"))) ios, _, stdout, stderr := iostreams.Test() gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extDir).Return(gcOne).Twice() gcOne.On("Remotes").Return(nil, nil).Once() - m := newTestManager(tempDir, nil, gc, ios) + + m := newTestManager(dataDir, updateDir, nil, gc, ios) m.EnableDryRunMode() exts, err := m.list(false) assert.NoError(t, err) @@ -382,16 +405,18 @@ func TestManager_UpgradeExtension_GitExtension_DryRun(t *testing.T) { } func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) { - tempDir := t.TempDir() - extensionDir := filepath.Join(tempDir, "extensions", "gh-remote") - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + extensionDir := filepath.Join(dataDir, "extensions", "gh-remote") + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-remote", "gh-remote"))) ios, _, stdout, stderr := iostreams.Test() gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extensionDir).Return(gcOne).Times(3) gcOne.On("Remotes").Return(nil, nil).Once() gcOne.On("Fetch", "origin", "HEAD").Return(nil).Once() gcOne.On("CommandOutput", []string{"reset", "--hard", "origin/HEAD"}).Return("", nil).Once() - m := newTestManager(tempDir, nil, gc, ios) + + m := newTestManager(dataDir, updateDir, nil, gc, ios) exts, err := m.list(false) assert.NoError(t, err) assert.Equal(t, 1, len(exts)) @@ -407,15 +432,17 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) { } func TestManager_MigrateToBinaryExtension(t *testing.T) { - tempDir := t.TempDir() - assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + dataDir := t.TempDir() + updateDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(dataDir, "extensions", "gh-remote", "gh-remote"))) ios, _, stdout, stderr := iostreams.Test() reg := httpmock.Registry{} defer reg.Verify(t) client := http.Client{Transport: ®} gc := &gitExecuter{client: &git.Client{}} - m := newTestManager(tempDir, &client, gc, ios) + + m := newTestManager(dataDir, updateDir, &client, gc, ios) exts, err := m.list(false) assert.NoError(t, err) assert.Equal(t, 1, len(exts)) @@ -463,7 +490,7 @@ func TestManager_MigrateToBinaryExtension(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-remote", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-remote", manifestName)) assert.NoError(t, err) var bm binManifest @@ -475,23 +502,24 @@ func TestManager_MigrateToBinaryExtension(t *testing.T) { Owner: "owner", Host: "github.com", Tag: "v1.0.2", - Path: filepath.Join(tempDir, "extensions/gh-remote/gh-remote.exe"), + Path: filepath.Join(dataDir, "extensions/gh-remote/gh-remote.exe"), }, bm) - fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-remote/gh-remote.exe")) + fakeBin, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-remote/gh-remote.exe")) assert.NoError(t, err) assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin)) } func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() reg := httpmock.Registry{} defer reg.Verify(t) assert.NoError(t, stubBinaryExtension( - filepath.Join(tempDir, "extensions", "gh-bin-ext"), + filepath.Join(dataDir, "extensions", "gh-bin-ext"), binManifest{ Owner: "owner", Name: "gh-bin-ext", @@ -500,7 +528,7 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { })) ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, &http.Client{Transport: ®}, nil, ios) + m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) reg.Register( httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), httpmock.JSONResponse( @@ -525,7 +553,7 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { err = m.upgradeExtension(ext, false) assert.NoError(t, err) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext", manifestName)) assert.NoError(t, err) var bm binManifest @@ -537,10 +565,10 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { Owner: "owner", Host: "example.com", Tag: "v1.0.2", - Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), + Path: filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), }, bm) - fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) + fakeBin, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) assert.NoError(t, err) assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin)) @@ -549,13 +577,14 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { } func TestManager_UpgradeExtension_BinaryExtension_Pinned_Force(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() reg := httpmock.Registry{} defer reg.Verify(t) assert.NoError(t, stubBinaryExtension( - filepath.Join(tempDir, "extensions", "gh-bin-ext"), + filepath.Join(dataDir, "extensions", "gh-bin-ext"), binManifest{ Owner: "owner", Name: "gh-bin-ext", @@ -565,7 +594,7 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned_Force(t *testing.T) { })) ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, &http.Client{Transport: ®}, nil, ios) + m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) reg.Register( httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), httpmock.JSONResponse( @@ -590,7 +619,7 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned_Force(t *testing.T) { err = m.upgradeExtension(ext, true) assert.NoError(t, err) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext", manifestName)) assert.NoError(t, err) var bm binManifest @@ -602,10 +631,10 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned_Force(t *testing.T) { Owner: "owner", Host: "example.com", Tag: "v1.0.2", - Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), + Path: filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), }, bm) - fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) + fakeBin, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) assert.NoError(t, err) assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin)) @@ -614,11 +643,12 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned_Force(t *testing.T) { } func TestManager_UpgradeExtension_BinaryExtension_DryRun(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() reg := httpmock.Registry{} defer reg.Verify(t) assert.NoError(t, stubBinaryExtension( - filepath.Join(tempDir, "extensions", "gh-bin-ext"), + filepath.Join(dataDir, "extensions", "gh-bin-ext"), binManifest{ Owner: "owner", Name: "gh-bin-ext", @@ -627,7 +657,7 @@ func TestManager_UpgradeExtension_BinaryExtension_DryRun(t *testing.T) { })) ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, &http.Client{Transport: ®}, nil, ios) + m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) m.EnableDryRunMode() reg.Register( httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), @@ -649,7 +679,7 @@ func TestManager_UpgradeExtension_BinaryExtension_DryRun(t *testing.T) { err = m.upgradeExtension(ext, false) assert.NoError(t, err) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext", manifestName)) assert.NoError(t, err) var bm binManifest @@ -667,10 +697,11 @@ func TestManager_UpgradeExtension_BinaryExtension_DryRun(t *testing.T) { } func TestManager_UpgradeExtension_BinaryExtension_Pinned(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() assert.NoError(t, stubBinaryExtension( - filepath.Join(tempDir, "extensions", "gh-bin-ext"), + filepath.Join(dataDir, "extensions", "gh-bin-ext"), binManifest{ Owner: "owner", Name: "gh-bin-ext", @@ -680,7 +711,7 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned(t *testing.T) { })) ios, _, _, _ := iostreams.Test() - m := newTestManager(tempDir, nil, nil, ios) + m := newTestManager(dataDir, updateDir, nil, nil, ios) exts, err := m.list(false) assert.Nil(t, err) assert.Equal(t, 1, len(exts)) @@ -692,8 +723,9 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned(t *testing.T) { } func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) { - tempDir := t.TempDir() - extDir := filepath.Join(tempDir, "extensions", "gh-remote") + dataDir := t.TempDir() + updateDir := t.TempDir() + extDir := filepath.Join(dataDir, "extensions", "gh-remote") assert.NoError(t, stubPinnedExtension(filepath.Join(extDir, "gh-remote"), "abcd1234")) ios, _, _, _ := iostreams.Test() @@ -701,7 +733,7 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) { gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extDir).Return(gcOne).Once() - m := newTestManager(tempDir, nil, gc, ios) + m := newTestManager(dataDir, updateDir, nil, gc, ios) exts, err := m.list(false) @@ -719,8 +751,33 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) { gcOne.AssertExpectations(t) } +func TestManager_InstallLocal(t *testing.T) { + dataDir := t.TempDir() + updateDir := t.TempDir() + + extDir := filepath.Join(t.TempDir(), "gh-local") + err := os.MkdirAll(extDir, 0755) + assert.NoErrorf(t, err, "failed to create local extension") + assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-local"))) + + ios, _, stdout, stderr := iostreams.Test() + m := newTestManager(dataDir, updateDir, nil, nil, ios) + + err = m.InstallLocal(extDir) + assert.NoError(t, err) + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) + + fm, err := os.Lstat(filepath.Join(dataDir, "extensions", "gh-local")) + assert.NoErrorf(t, err, "data directory missing local extension symlink") + isSymlink := fm.Mode() & os.ModeSymlink + assert.True(t, isSymlink != 0) + assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-local"), "update directory should be removed") +} + func TestManager_Install_git(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() reg := httpmock.Registry{} defer reg.Verify(t) @@ -728,11 +785,12 @@ func TestManager_Install_git(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() - extensionDir := filepath.Join(tempDir, "extensions", "gh-some-ext") + extensionDir := filepath.Join(dataDir, "extensions", "gh-some-ext") gc := &mockGitClient{} gc.On("Clone", "https://github.com/owner/gh-some-ext.git", []string{extensionDir}).Return("", nil).Once() + assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-some-ext"))) - m := newTestManager(tempDir, &client, gc, ios) + m := newTestManager(dataDir, updateDir, &client, gc, ios) reg.Register( httpmock.REST("GET", "repos/owner/gh-some-ext/releases/latest"), @@ -756,10 +814,13 @@ func TestManager_Install_git(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) gc.AssertExpectations(t) + + assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-some-ext"), "update directory should be removed") } func TestManager_Install_git_pinned(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() reg := httpmock.Registry{} defer reg.Verify(t) @@ -767,13 +828,13 @@ func TestManager_Install_git_pinned(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() - extensionDir := filepath.Join(tempDir, "extensions", "gh-cool-ext") + extensionDir := filepath.Join(dataDir, "extensions", "gh-cool-ext") gc, gcOne := &mockGitClient{}, &mockGitClient{} gc.On("ForRepo", extensionDir).Return(gcOne).Once() gc.On("Clone", "https://github.com/owner/gh-cool-ext.git", []string{extensionDir}).Return("", nil).Once() gcOne.On("CheckoutBranch", "abcd1234").Return(nil).Once() - m := newTestManager(tempDir, &client, gc, ios) + m := newTestManager(dataDir, updateDir, &client, gc, ios) reg.Register( httpmock.REST("GET", "repos/owner/gh-cool-ext/releases/latest"), @@ -837,14 +898,15 @@ func TestManager_Install_binary_pinned(t *testing.T) { httpmock.StringResponse("FAKE BINARY")) ios, _, stdout, stderr := iostreams.Test() - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() - m := newTestManager(tempDir, &http.Client{Transport: ®}, nil, ios) + m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) err := m.Install(repo, "v1.6.3-pre") assert.NoError(t, err) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext", manifestName)) assert.NoError(t, err) var bm binManifest @@ -857,10 +919,10 @@ func TestManager_Install_binary_pinned(t *testing.T) { Host: "example.com", Tag: "v1.6.3-pre", IsPinned: true, - Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), + Path: filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), }, bm) - fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) + fakeBin, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) assert.NoError(t, err) assert.Equal(t, "FAKE BINARY", string(fakeBin)) @@ -901,9 +963,10 @@ func TestManager_Install_binary_unsupported(t *testing.T) { })) ios, _, stdout, stderr := iostreams.Test() - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() - m := newTestManager(tempDir, &client, nil, ios) + m := newTestManager(dataDir, updateDir, &client, nil, ios) err := m.Install(repo, "") assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64.\n\nTo request support for windows-amd64, open an issue on the extension's repo by running the following command:\n\n\t`gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .\"`") @@ -944,9 +1007,10 @@ func TestManager_Install_rosetta_fallback_not_found(t *testing.T) { })) ios, _, stdout, stderr := iostreams.Test() - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() - m := newTestManager(tempDir, &client, nil, ios) + m := newTestManager(dataDir, updateDir, &client, nil, ios) m.platform = func() (string, string) { return "darwin-arm64", "" } @@ -998,14 +1062,16 @@ func TestManager_Install_binary(t *testing.T) { httpmock.StringResponse("FAKE BINARY")) ios, _, stdout, stderr := iostreams.Test() - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() + assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-bin-ext"))) - m := newTestManager(tempDir, &http.Client{Transport: ®}, nil, ios) + m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) err := m.Install(repo, "") assert.NoError(t, err) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext", manifestName)) assert.NoError(t, err) var bm binManifest @@ -1017,15 +1083,17 @@ func TestManager_Install_binary(t *testing.T) { Owner: "owner", Host: "example.com", Tag: "v1.0.1", - Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), + Path: filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe"), }, bm) - fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) + fakeBin, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) assert.NoError(t, err) assert.Equal(t, "FAKE BINARY", string(fakeBin)) assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) + + assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-bin-ext"), "update directory should be removed") } func TestManager_Install_amd64_when_supported(t *testing.T) { @@ -1063,9 +1131,10 @@ func TestManager_Install_amd64_when_supported(t *testing.T) { httpmock.StringResponse("FAKE BINARY")) ios, _, stdout, stderr := iostreams.Test() - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() - m := newTestManager(tempDir, &client, nil, ios) + m := newTestManager(dataDir, updateDir, &client, nil, ios) m.platform = func() (string, string) { return "darwin-arm64", "" } @@ -1079,7 +1148,7 @@ func TestManager_Install_amd64_when_supported(t *testing.T) { err := m.Install(repo, "") assert.NoError(t, err) - manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + manifest, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext", manifestName)) assert.NoError(t, err) var bm binManifest @@ -1091,10 +1160,10 @@ func TestManager_Install_amd64_when_supported(t *testing.T) { Owner: "owner", Host: "example.com", Tag: "v1.0.1", - Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"), + Path: filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext"), }, bm) - fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext")) + fakeBin, err := os.ReadFile(filepath.Join(dataDir, "extensions/gh-bin-ext/gh-bin-ext")) assert.NoError(t, err) assert.Equal(t, "FAKE BINARY", string(fakeBin)) @@ -1116,9 +1185,10 @@ func TestManager_repo_not_found(t *testing.T) { httpmock.StatusStringResponse(404, `{}`)) ios, _, stdout, stderr := iostreams.Test() - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() - m := newTestManager(tempDir, &http.Client{Transport: ®}, nil, ios) + m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) if err := m.Install(repo, ""); err != repositoryNotFoundErr { t.Errorf("expected repositoryNotFoundErr, got: %v", err) @@ -1141,7 +1211,8 @@ func TestManager_Create(t *testing.T) { gcOne.On("CommandOutput", []string{"add", "gh-test", "--chmod=+x"}).Return("", nil).Once() gcOne.On("CommandOutput", []string{"commit", "-m", "initial commit"}).Return("", nil).Once() - m := newTestManager(".", nil, gc, ios) + updateDir := t.TempDir() + m := newTestManager(".", updateDir, nil, gc, ios) err = m.Create("gh-test", extensions.GitTemplateType) assert.NoError(t, err) @@ -1174,7 +1245,8 @@ func TestManager_Create_go_binary(t *testing.T) { gcOne.On("CommandOutput", []string{"add", "."}).Return("", nil).Once() gcOne.On("CommandOutput", []string{"commit", "-m", "initial commit"}).Return("", nil).Once() - m := newTestManager(".", &http.Client{Transport: ®}, gc, ios) + updateDir := t.TempDir() + m := newTestManager(".", updateDir, &http.Client{Transport: ®}, gc, ios) err = m.Create("gh-test", extensions.GoBinTemplateType) require.NoError(t, err) @@ -1218,7 +1290,8 @@ func TestManager_Create_other_binary(t *testing.T) { gcOne.On("CommandOutput", []string{"add", "."}).Return("", nil).Once() gcOne.On("CommandOutput", []string{"commit", "-m", "initial commit"}).Return("", nil).Once() - m := newTestManager(".", nil, gc, ios) + updateDir := t.TempDir() + m := newTestManager(".", updateDir, nil, gc, ios) err = m.Create("gh-test", extensions.OtherBinTemplateType) assert.NoError(t, err) @@ -1241,6 +1314,41 @@ func TestManager_Create_other_binary(t *testing.T) { gcOne.AssertExpectations(t) } +func Test_ensurePrefixed(t *testing.T) { + tests := []struct { + name string + input string + expected string + wantErr bool + }{ + { + name: "missing gh- prefix", + input: "bad-kitty", + expected: "gh-bad-kitty", + }, + { + name: "has gh- prefix", + input: "gh-purrfect", + expected: "gh-purrfect", + }, + { + name: "empty string", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := ensurePrefixed(tt.input) + if tt.wantErr { + require.Error(t, err) + } else { + require.Equal(t, tt.expected, actual) + } + }) + } +} + // chdirTemp changes the current working directory to a temporary directory for the duration of the test. func chdirTemp(t *testing.T) { oldWd, _ := os.Getwd() @@ -1352,3 +1460,13 @@ func stubBinaryExtension(installPath string, bm binManifest) error { return fm.Close() } + +func stubExtensionUpdate(updatePath string) error { + if _, err := os.Stat(updatePath); err == nil { + return fmt.Errorf("failed to stub extension update directory: %s already exists", updatePath) + } + if err := os.MkdirAll(updatePath, 0755); err != nil { + return fmt.Errorf("failed to stub extension update directory: %w", err) + } + return nil +} diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index a0b3b5755bd..0eda58c2cd2 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -4,11 +4,9 @@ import ( "errors" "fmt" "os/exec" - "path/filepath" "strings" "time" - "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/update" "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/iostreams" @@ -83,6 +81,5 @@ func checkForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte return nil, nil } - stateFilePath := filepath.Join(config.StateDir(), "extensions", ext.FullName(), "state.yml") - return update.CheckForExtensionUpdate(em, ext, stateFilePath, time.Now()) + return update.CheckForExtensionUpdate(em, ext, time.Now()) } diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index 5c590314022..db6c75098c3 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -39,4 +39,5 @@ type ExtensionManager interface { Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) Create(name string, tmplType ExtTemplateType) error EnableDryRunMode() + UpdateDir(name string) string } diff --git a/pkg/extensions/manager_mock.go b/pkg/extensions/manager_mock.go index 962444f2b1c..81f0ba3f7d6 100644 --- a/pkg/extensions/manager_mock.go +++ b/pkg/extensions/manager_mock.go @@ -40,6 +40,9 @@ var _ ExtensionManager = &ExtensionManagerMock{} // RemoveFunc: func(name string) error { // panic("mock out the Remove method") // }, +// UpdateDirFunc: func(name string) string { +// panic("mock out the UpdateDir method") +// }, // UpgradeFunc: func(name string, force bool) error { // panic("mock out the Upgrade method") // }, @@ -71,6 +74,9 @@ type ExtensionManagerMock struct { // RemoveFunc mocks the Remove method. RemoveFunc func(name string) error + // UpdateDirFunc mocks the UpdateDir method. + UpdateDirFunc func(name string) string + // UpgradeFunc mocks the Upgrade method. UpgradeFunc func(name string, force bool) error @@ -117,6 +123,11 @@ type ExtensionManagerMock struct { // Name is the name argument value. Name string } + // UpdateDir holds details about calls to the UpdateDir method. + UpdateDir []struct { + // Name is the name argument value. + Name string + } // Upgrade holds details about calls to the Upgrade method. Upgrade []struct { // Name is the name argument value. @@ -132,6 +143,7 @@ type ExtensionManagerMock struct { lockInstallLocal sync.RWMutex lockList sync.RWMutex lockRemove sync.RWMutex + lockUpdateDir sync.RWMutex lockUpgrade sync.RWMutex } @@ -369,6 +381,38 @@ func (mock *ExtensionManagerMock) RemoveCalls() []struct { return calls } +// UpdateDir calls UpdateDirFunc. +func (mock *ExtensionManagerMock) UpdateDir(name string) string { + if mock.UpdateDirFunc == nil { + panic("ExtensionManagerMock.UpdateDirFunc: method is nil but ExtensionManager.UpdateDir was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockUpdateDir.Lock() + mock.calls.UpdateDir = append(mock.calls.UpdateDir, callInfo) + mock.lockUpdateDir.Unlock() + return mock.UpdateDirFunc(name) +} + +// UpdateDirCalls gets all the calls that were made to UpdateDir. +// Check the length with: +// +// len(mockedExtensionManager.UpdateDirCalls()) +func (mock *ExtensionManagerMock) UpdateDirCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockUpdateDir.RLock() + calls = mock.calls.UpdateDir + mock.lockUpdateDir.RUnlock() + return calls +} + // Upgrade calls UpgradeFunc. func (mock *ExtensionManagerMock) Upgrade(name string, force bool) error { if mock.UpgradeFunc == nil { From a55f50b1154ee391f991d6677949c1091c3970d5 Mon Sep 17 00:00:00 2001 From: Andrew Feller Date: Sun, 8 Dec 2024 20:01:26 -0500 Subject: [PATCH 0020/2338] Remove Extension.FullName() from interface - this was something I did in the original implementation of this improved extension update notification logic - discovering other parts of the extension manager code that were working with non-prefixed extension names motivated a different approach - the extension manager code that requires the extension be prefixed has been enhanced to use the centralized ensurePrefixed() logic, making the need for this on the extension unnecessary --- cmd/gen-docs/main.go | 4 ++++ internal/update/update.go | 2 +- internal/update/update_test.go | 6 ++--- pkg/cmd/extension/manager.go | 14 ++++-------- pkg/cmd/extension/manager_test.go | 11 +-------- pkg/cmd/root/extension_test.go | 3 --- pkg/extensions/extension.go | 5 ++--- pkg/extensions/extension_mock.go | 37 ------------------------------- 8 files changed, 14 insertions(+), 68 deletions(-) diff --git a/cmd/gen-docs/main.go b/cmd/gen-docs/main.go index 0c9590fac5f..60fd8af58d6 100644 --- a/cmd/gen-docs/main.go +++ b/cmd/gen-docs/main.go @@ -127,3 +127,7 @@ func (e *em) Create(_ string, _ extensions.ExtTemplateType) error { } func (e *em) EnableDryRunMode() {} + +func (e *em) UpdateDir(_ string) string { + return "" +} diff --git a/internal/update/update.go b/internal/update/update.go index 6010689c7aa..f9a97917a4c 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -48,7 +48,7 @@ func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte return nil, nil } - stateFilePath := filepath.Join(em.UpdateDir(ext.FullName()), "state.yml") // TODO: See what removing FullName() changes + stateFilePath := filepath.Join(em.UpdateDir(ext.Name()), "state.yml") stateEntry, _ := getStateEntry(stateFilePath) if stateEntry != nil && now.Sub(stateEntry.CheckedForUpdateAt).Hours() < 24 { return nil, nil diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 6f7db6e701d..28a31aa4168 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -203,6 +203,7 @@ func TestCheckForExtensionUpdate(t *testing.T) { expectedReleaseInfo: nil, }, // TODO: Local extension with no previous state entry + // TODO: Capture git and binary specific entries } for _, tt := range tests { @@ -218,9 +219,6 @@ func TestCheckForExtensionUpdate(t *testing.T) { NameFunc: func() string { return "extension-update-test" }, - FullNameFunc: func() string { - return "gh-extension-update-test" - }, CurrentVersionFunc: func() string { return tt.extCurrentVersion }, @@ -248,7 +246,7 @@ func TestCheckForExtensionUpdate(t *testing.T) { } // Setup previous state file for test as necessary - stateFilePath := filepath.Join(em.UpdateDir(ext.FullName()), "state.yml") + stateFilePath := filepath.Join(em.UpdateDir(ext.Name()), "state.yml") if tt.previousStateEntry != nil { require.NoError(t, setStateEntry(stateFilePath, tt.previousStateEntry.CheckedForUpdateAt, tt.previousStateEntry.LatestRelease)) } diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 4670e120f85..ccb4b9bfa10 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -542,10 +542,7 @@ func (m *Manager) upgradeBinExtension(ext *Extension) error { } func (m *Manager) Remove(name string) error { - name, err := ensurePrefixed(name) - if err != nil { - return err - } + name = ensurePrefixed(name) targetDir := filepath.Join(m.installDir(), name) if _, err := os.Lstat(targetDir); os.IsNotExist(err) { return fmt.Errorf("no extension found: %q", targetDir) @@ -565,7 +562,7 @@ func (m *Manager) installDir() string { // UpdateDir returns the extension-specific directory where updates are stored. func (m *Manager) UpdateDir(name string) string { - return filepath.Join(m.updateDir(), name) + return filepath.Join(m.updateDir(), ensurePrefixed(name)) } //go:embed ext_tmpls/goBinMain.go.txt @@ -842,12 +839,9 @@ func (m *Manager) cleanExtensionUpdateDir(name string) error { } // ensurePrefixed just makes sure that the provided extension name is prefixed with "gh-". -func ensurePrefixed(name string) (string, error) { - if name == "" { - return "", errors.New("failed prefixing extension name: must not be empty") - } +func ensurePrefixed(name string) string { if !strings.HasPrefix(name, "gh-") { name = "gh-" + name } - return name, nil + return name } diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 9564fc6fa43..af82646396d 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -1331,20 +1331,11 @@ func Test_ensurePrefixed(t *testing.T) { input: "gh-purrfect", expected: "gh-purrfect", }, - { - name: "empty string", - wantErr: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual, err := ensurePrefixed(tt.input) - if tt.wantErr { - require.Error(t, err) - } else { - require.Equal(t, tt.expected, actual) - } + require.Equal(t, tt.expected, ensurePrefixed(tt.input)) }) } } diff --git a/pkg/cmd/root/extension_test.go b/pkg/cmd/root/extension_test.go index e71c63317fa..1d74c129771 100644 --- a/pkg/cmd/root/extension_test.go +++ b/pkg/cmd/root/extension_test.go @@ -137,9 +137,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { CurrentVersionFunc: func() string { return tt.extCurrentVersion }, - FullNameFunc: func() string { - return tt.extFullName - }, IsPinnedFunc: func() bool { return tt.extIsPinned }, diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index db6c75098c3..1b9c3461204 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -16,9 +16,8 @@ const ( //go:generate moq -rm -out extension_mock.go . Extension type Extension interface { - Name() string // Extension Name without gh- - FullName() string // Extension Name with gh- - Path() string // Path to executable + Name() string // Extension Name without gh- + Path() string // Path to executable URL() string CurrentVersion() string LatestVersion() string diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go index 2488c44ebff..9b473e20ae2 100644 --- a/pkg/extensions/extension_mock.go +++ b/pkg/extensions/extension_mock.go @@ -20,9 +20,6 @@ var _ Extension = &ExtensionMock{} // CurrentVersionFunc: func() string { // panic("mock out the CurrentVersion method") // }, -// FullNameFunc: func() string { -// panic("mock out the FullName method") -// }, // IsBinaryFunc: func() bool { // panic("mock out the IsBinary method") // }, @@ -60,9 +57,6 @@ type ExtensionMock struct { // CurrentVersionFunc mocks the CurrentVersion method. CurrentVersionFunc func() string - // FullNameFunc mocks the FullName method. - FullNameFunc func() string - // IsBinaryFunc mocks the IsBinary method. IsBinaryFunc func() bool @@ -95,9 +89,6 @@ type ExtensionMock struct { // CurrentVersion holds details about calls to the CurrentVersion method. CurrentVersion []struct { } - // FullName holds details about calls to the FullName method. - FullName []struct { - } // IsBinary holds details about calls to the IsBinary method. IsBinary []struct { } @@ -127,7 +118,6 @@ type ExtensionMock struct { } } lockCurrentVersion sync.RWMutex - lockFullName sync.RWMutex lockIsBinary sync.RWMutex lockIsLocal sync.RWMutex lockIsPinned sync.RWMutex @@ -166,33 +156,6 @@ func (mock *ExtensionMock) CurrentVersionCalls() []struct { return calls } -// FullName calls FullNameFunc. -func (mock *ExtensionMock) FullName() string { - if mock.FullNameFunc == nil { - panic("ExtensionMock.FullNameFunc: method is nil but Extension.FullName was just called") - } - callInfo := struct { - }{} - mock.lockFullName.Lock() - mock.calls.FullName = append(mock.calls.FullName, callInfo) - mock.lockFullName.Unlock() - return mock.FullNameFunc() -} - -// FullNameCalls gets all the calls that were made to FullName. -// Check the length with: -// -// len(mockedExtension.FullNameCalls()) -func (mock *ExtensionMock) FullNameCalls() []struct { -} { - var calls []struct { - } - mock.lockFullName.RLock() - calls = mock.calls.FullName - mock.lockFullName.RUnlock() - return calls -} - // IsBinary calls IsBinaryFunc. func (mock *ExtensionMock) IsBinary() bool { if mock.IsBinaryFunc == nil { From a79e086bd45159724d3c582929a19a10dfe41a6d Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Sun, 8 Dec 2024 20:43:51 -0500 Subject: [PATCH 0021/2338] Test whether extension update should be checked --- internal/update/update_test.go | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 28a31aa4168..7e04e55e90a 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -265,6 +265,63 @@ func TestCheckForExtensionUpdate(t *testing.T) { } } +func TestShouldCheckForExtensionUpdate(t *testing.T) { + tests := []struct { + name string + env map[string]string + expected bool + }{ + { + name: "should not check when user has explicitly disable notifications", + env: map[string]string{ + "GH_NO_UPDATE_NOTIFIER": "1", + }, + expected: false, + }, + { + name: "should not check when user is in codespace", + env: map[string]string{ + "CODESPACES": "1", + }, + expected: false, + }, + { + name: "should not check when in GitHub Actions / Travis / Circle / Cirrus / GitLab / AppVeyor / CodeShip / dsari", + env: map[string]string{ + "CI": "1", + }, + expected: false, + }, + { + name: "should not check when in Jenkins / TeamCity", + env: map[string]string{ + "BUILD_NUMBER": "1", + }, + expected: false, + }, + { + name: "should not check when in TaskCluster / dsari", + env: map[string]string{ + "RUN_ID": "1", + }, + expected: false, + }, + // TODO: Figure out how to refactor IsTerminal() to be testable + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Clearenv() + for k, v := range tt.env { + os.Setenv(k, v) + } + + actual := ShouldCheckForExtensionUpdate() + require.Equal(t, tt.expected, actual) + }) + } +} + func tempFilePath() string { file, err := os.CreateTemp("", "") if err != nil { From 10f62b1c37d588eae076b731df4a95d7316d495b Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Sun, 8 Dec 2024 21:06:01 -0500 Subject: [PATCH 0022/2338] Expand update tests for local extensions --- internal/update/update_test.go | 127 +++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 12 deletions(-) diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 7e04e55e90a..e40a09c0c7f 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/cli/cli/v2/pkg/cmd/extension" "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/require" @@ -130,7 +131,7 @@ func TestCheckForExtensionUpdate(t *testing.T) { name string extCurrentVersion string extLatestVersion string - extIsLocal bool + extKind extension.ExtensionKind extURL string previousStateEntry *StateEntry expectedStateEntry *StateEntry @@ -138,10 +139,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { wantErr bool }{ { - name: "return latest release given extension is out of date and no state entry", + name: "return latest release given git extension is out of date and no state entry", extCurrentVersion: "v0.1.0", extLatestVersion: "v1.0.0", - extIsLocal: false, + extKind: extension.GitKind, extURL: "http://example.com", expectedStateEntry: &StateEntry{ CheckedForUpdateAt: now, @@ -156,10 +157,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { }, }, { - name: "return latest release given extension is out of date and state entry is old enough", + name: "return latest release given git extension is out of date and state entry is old enough", extCurrentVersion: "v0.1.0", extLatestVersion: "v1.0.0", - extIsLocal: false, + extKind: extension.GitKind, extURL: "http://example.com", previousStateEntry: &StateEntry{ CheckedForUpdateAt: previousOldEnough, @@ -181,10 +182,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { }, }, { - name: "return nothing given extension is out of date but state entry is too recent", + name: "return nothing given git extension is out of date but state entry is too recent", extCurrentVersion: "v0.1.0", extLatestVersion: "v1.0.0", - extIsLocal: false, + extKind: extension.GitKind, extURL: "http://example.com", previousStateEntry: &StateEntry{ CheckedForUpdateAt: previousTooSoon, @@ -202,8 +203,102 @@ func TestCheckForExtensionUpdate(t *testing.T) { }, expectedReleaseInfo: nil, }, - // TODO: Local extension with no previous state entry - // TODO: Capture git and binary specific entries + { + name: "return latest release given binary extension is out of date and no state entry", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extKind: extension.BinaryKind, + extURL: "http://example.com", + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: now, + LatestRelease: ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + }, + expectedReleaseInfo: &ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + }, + { + name: "return latest release given binary extension is out of date and state entry is old enough", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extKind: extension.BinaryKind, + extURL: "http://example.com", + previousStateEntry: &StateEntry{ + CheckedForUpdateAt: previousOldEnough, + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: now, + LatestRelease: ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + }, + expectedReleaseInfo: &ReleaseInfo{ + Version: "v1.0.0", + URL: "http://example.com", + }, + }, + { + name: "return nothing given binary extension is out of date but state entry is too recent", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extKind: extension.BinaryKind, + extURL: "http://example.com", + previousStateEntry: &StateEntry{ + CheckedForUpdateAt: previousTooSoon, + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: previousTooSoon, + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedReleaseInfo: nil, + }, + { + name: "return nothing given local extension with no state entry", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extKind: extension.LocalKind, + extURL: "http://example.com", + expectedStateEntry: nil, + expectedReleaseInfo: nil, + }, + { + name: "return nothing given local extension despite state entry is old enough", + extCurrentVersion: "v0.1.0", + extLatestVersion: "v1.0.0", + extKind: extension.LocalKind, + extURL: "http://example.com", + previousStateEntry: &StateEntry{ + CheckedForUpdateAt: previousOldEnough, + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedStateEntry: &StateEntry{ + CheckedForUpdateAt: previousOldEnough, + LatestRelease: ReleaseInfo{ + Version: "v0.1.0", + URL: "http://example.com", + }, + }, + expectedReleaseInfo: nil, + }, } for _, tt := range tests { @@ -226,7 +321,10 @@ func TestCheckForExtensionUpdate(t *testing.T) { return tt.extLatestVersion }, IsLocalFunc: func() bool { - return tt.extIsLocal + return tt.extKind == extension.LocalKind + }, + IsBinaryFunc: func() bool { + return tt.extKind == extension.BinaryKind }, URLFunc: func() string { return tt.extURL @@ -254,9 +352,14 @@ func TestCheckForExtensionUpdate(t *testing.T) { actual, err := CheckForExtensionUpdate(em, ext, now) if tt.wantErr { require.Error(t, err) - } else { - require.Equal(t, tt.expectedReleaseInfo, actual) + return + } + require.Equal(t, tt.expectedReleaseInfo, actual) + + if tt.expectedStateEntry == nil { + require.NoFileExists(t, stateFilePath) + } else { stateEntry, err := getStateEntry(stateFilePath) require.NoError(t, err) require.Equal(t, tt.expectedStateEntry, stateEntry) From a6b8bec70d6b23997def1e0de7014a5653317b65 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Sun, 8 Dec 2024 21:17:26 -0500 Subject: [PATCH 0023/2338] Remove unnecessary Extension.FullName() --- pkg/cmd/extension/extension.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index 5fada887c1e..dc7ffa8c3a3 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -41,11 +41,7 @@ type Extension struct { } func (e *Extension) Name() string { - return strings.TrimPrefix(e.FullName(), "gh-") -} - -func (e *Extension) FullName() string { - return filepath.Base(e.path) + return strings.TrimPrefix(filepath.Base(e.path), "gh-") } func (e *Extension) Path() string { From 4bc90101cd11ccf7a4e8a26c014d2f759dfc7108 Mon Sep 17 00:00:00 2001 From: nilvng Date: Mon, 9 Dec 2024 21:20:07 +1100 Subject: [PATCH 0024/2338] issue #2329: fix prompt header getting duplicated following the approach in gh run view seems to fix this issue --- pkg/cmd/pr/checkout/checkout.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index e60658955b8..71af45ddaa2 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -102,7 +102,7 @@ func checkoutRun(opts *CheckoutOptions) error { return err } - pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder) + pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.IO) if err != nil { return err } @@ -291,7 +291,7 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm return nil } -func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder) (*api.PullRequest, error) { +func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, io *iostreams.IOStreams) (*api.PullRequest, error) { // When non-interactive if pullRequestSelector != "" { pr, _, err := pullRequestFinder.Find(shared.FindOptions{ @@ -314,6 +314,7 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar return nil, cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively") } // When interactive + io.StartProgressIndicator() listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{ "number", "title", @@ -328,6 +329,7 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar "isCrossRepository", "maintainerCanModify", }}, 10) + io.StopProgressIndicator() if err != nil { return nil, err } From 5109336963c445204b7eeee72ed20dc095a33cf7 Mon Sep 17 00:00:00 2001 From: nilvng Date: Mon, 9 Dec 2024 21:22:00 +1100 Subject: [PATCH 0025/2338] issue #2329: include PR status in the prompt's options --- pkg/cmd/pr/checkout/checkout.go | 3 +- pkg/cmd/pr/checkout/checkout_test.go | 4 +- pkg/cmd/pr/checkout/fixtures/prList.json | 117 +++++++++++------------ pkg/cmd/pr/list/list.go | 10 +- pkg/cmd/pr/shared/display.go | 8 ++ 5 files changed, 71 insertions(+), 71 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 71af45ddaa2..88850b940f0 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -342,8 +342,9 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar func promptForPR(prompter shared.Prompter, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { candidates := []string{} for _, pr := range jobs.PullRequests { - candidates = append(candidates, fmt.Sprintf("#%d %s [%s]", + candidates = append(candidates, fmt.Sprintf("%d\t%s\t%s [%s]", pr.Number, + shared.PrStateWithDraft(&pr), text.RemoveExcessiveWhitespace(pr.Title), pr.HeadLabel(), )) diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 33173252e7b..f2b37991233 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -239,9 +239,9 @@ func Test_checkoutRun(t *testing.T) { }, promptStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Select a pull request", - []string{"#32 New feature [feature]", "#29 Fixed bad bug [bug-fix]", "#28 Improve documentation [docs]"}, + []string{"32\tDRAFT\tNew feature [feature]", "29\tOPEN\tFixed bad bug [bug-fix]", "28\tOPEN\tImprove documentation [docs]"}, func(_, _ string, opts []string) (int, error) { - return prompter.IndexFor(opts, "#32 New feature [feature]") + return prompter.IndexFor(opts, "32\tDRAFT\tNew feature [feature]") }) }, runStubs: func(cs *run.CommandStubber) { diff --git a/pkg/cmd/pr/checkout/fixtures/prList.json b/pkg/cmd/pr/checkout/fixtures/prList.json index f915df136f6..84928722c53 100644 --- a/pkg/cmd/pr/checkout/fixtures/prList.json +++ b/pkg/cmd/pr/checkout/fixtures/prList.json @@ -1,67 +1,66 @@ { - "data": { - "repository": { - "pullRequests": { - "totalCount": 3, - "nodes": [ - { - "number": 32, - "title": "New feature", - "url": "https://github.com/OWNER/REPO/pull/32", - "createdAt": "2022-08-24T20:01:12Z", - "headRefName": "feature", - "state": "OPEN", - "isDraft": true, - "isCrossRepository": false, - "headRepository": { - "name": "REPO" - }, - "headRepositoryOwner": { - "login": "OWNER" - }, - "maintainerCanModify": true + "data": { + "repository": { + "pullRequests": { + "totalCount": 3, + "nodes": [ + { + "number": 32, + "title": "New feature", + "url": "https://github.com/OWNER/REPO/pull/32", + "createdAt": "2022-08-24T20:01:12Z", + "headRefName": "feature", + "state": "OPEN", + "isDraft": true, + "isCrossRepository": false, + "headRepository": { + "name": "REPO" }, - { - "number": 29, - "title": "Fixed bad bug", - "url": "https://github.com/OWNER/REPO/pull/29", - "createdAt": "2022-07-20T19:01:12Z", - "headRefName": "bug-fix", - "state": "OPEN", - "isDraft": false, - "isCrossRepository": false, - "headRepository": { - "name": "REPO" - }, - "headRepositoryOwner": { - "login": "OWNER" - }, - "maintainerCanModify": true + "headRepositoryOwner": { + "login": "OWNER" }, - { - "number": 28, - "state": "MERGED", - "isDraft": false, - "title": "Improve documentation", - "createdAt": "2020-01-26T19:01:12Z", - "url": "https://github.com/OWNER/REPO/pull/28", - "isCrossRepository": false, - "headRefName": "docs", - "headRepository": { - "name": "REPO" - }, - "headRepositoryOwner": { - "login": "OWNER" - }, - "maintainerCanModify": true - } - ], - "pageInfo": { - "hasNextPage": false, - "endCursor": "" + "maintainerCanModify": true + }, + { + "number": 29, + "title": "Fixed bad bug", + "url": "https://github.com/OWNER/REPO/pull/29", + "createdAt": "2022-07-20T19:01:12Z", + "headRefName": "bug-fix", + "state": "OPEN", + "isDraft": false, + "isCrossRepository": false, + "headRepository": { + "name": "REPO" + }, + "headRepositoryOwner": { + "login": "OWNER" + }, + "maintainerCanModify": true + }, + { + "number": 28, + "state": "OPEN", + "isDraft": false, + "title": "Improve documentation", + "createdAt": "2020-01-26T19:01:12Z", + "url": "https://github.com/OWNER/REPO/pull/28", + "isCrossRepository": false, + "headRefName": "docs", + "headRepository": { + "name": "REPO" + }, + "headRepositoryOwner": { + "login": "OWNER" + }, + "maintainerCanModify": true } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": "" } } } } - \ No newline at end of file +} diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 401e168e31a..e5ebe50fb96 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -222,7 +222,7 @@ func listRun(opts *ListOptions) error { table.AddField(text.RemoveExcessiveWhitespace(pr.Title)) table.AddField(pr.HeadLabel(), tableprinter.WithColor(cs.Cyan)) if !isTTY { - table.AddField(prStateWithDraft(&pr)) + table.AddField(shared.PrStateWithDraft(&pr)) } table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Gray) table.EndRow() @@ -234,11 +234,3 @@ func listRun(opts *ListOptions) error { return nil } - -func prStateWithDraft(pr *api.PullRequest) string { - if pr.IsDraft && pr.State == "OPEN" { - return "DRAFT" - } - - return pr.State -} diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index 7bd306104c0..02482951c5c 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -17,6 +17,14 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { return prStateColorFunc(text.Title(pr.State)) } +func PrStateWithDraft(pr *api.PullRequest) string { + if pr.IsDraft && pr.State == "OPEN" { + return "DRAFT" + } + + return pr.State +} + func ColorForPRState(pr api.PullRequest) string { switch pr.State { case "OPEN": From cbaaf77822bd764dbce526acce4a0b51d772fcb9 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Mon, 9 Dec 2024 08:35:01 -0300 Subject: [PATCH 0026/2338] fixing gh gist view prompts with no TTY --- pkg/cmd/gist/view/view.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 2a19f595859..a6f4d528a12 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -55,7 +55,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman } if !opts.IO.IsStdoutTTY() { - opts.Raw = true + return cmdutil.FlagErrorf("run or gist ID required when not running interactively\n\nUsage: gh gist view [ | ] [flags]\n\nFlags:\n -f, --filename string Display a single file from the gist\n --files List file names from the gist\n -r, --raw Print raw instead of rendered gist contents\n -w, --web Open gist in the browser\n") } if runF != nil { From b740486b13046ad1560aa1c461947ef27c145eb2 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Mon, 9 Dec 2024 10:59:09 -0300 Subject: [PATCH 0027/2338] #10042 fixed test --- pkg/cmd/gist/view/view_test.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 52e616f7cbb..2e80ee082cb 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -20,10 +20,11 @@ import ( func TestNewCmdView(t *testing.T) { tests := []struct { - name string - cli string - wants ViewOptions - tty bool + name string + cli string + wants ViewOptions + tty bool + wantsErr bool }{ { name: "tty no arguments", @@ -37,12 +38,14 @@ func TestNewCmdView(t *testing.T) { }, { name: "nontty no arguments", + tty: false, cli: "123", wants: ViewOptions{ Raw: true, Selector: "123", ListFiles: false, }, + wantsErr: true, }, { name: "filename passed", @@ -94,6 +97,12 @@ func TestNewCmdView(t *testing.T) { gotOpts = opts return nil }) + + _, err = cmd.ExecuteC() + if tt.wantsErr { + assert.Error(t, err) + return + } cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{}) From 52b845bcadad361815b46cbe3227c0348c57ce73 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 10 Dec 2024 22:13:48 -0500 Subject: [PATCH 0028/2338] Refactor local installation manager test by OS - Local extensions on Windows are not symlinks but files whose contents is the path to the local extension directory - Local extensions elsewhere are symlinks --- pkg/cmd/extension/manager_other_test.go | 34 ++++++++++++++++++++ pkg/cmd/extension/manager_test.go | 24 -------------- pkg/cmd/extension/manager_windows_test.go | 38 +++++++++++++++++++++++ 3 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 pkg/cmd/extension/manager_other_test.go create mode 100644 pkg/cmd/extension/manager_windows_test.go diff --git a/pkg/cmd/extension/manager_other_test.go b/pkg/cmd/extension/manager_other_test.go new file mode 100644 index 00000000000..48ccb349bf5 --- /dev/null +++ b/pkg/cmd/extension/manager_other_test.go @@ -0,0 +1,34 @@ +package extension + +import ( + "os" + "path/filepath" + "testing" + + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/require" +) + +func TestManager_InstallLocal(t *testing.T) { + dataDir := t.TempDir() + updateDir := t.TempDir() + + extDir := filepath.Join(t.TempDir(), "gh-local") + err := os.MkdirAll(extDir, 0755) + require.NoErrorf(t, err, "failed to create local extension") + require.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-local"))) + + ios, _, stdout, stderr := iostreams.Test() + m := newTestManager(dataDir, updateDir, nil, nil, ios) + + err = m.InstallLocal(extDir) + require.NoError(t, err) + require.Equal(t, "", stdout.String()) + require.Equal(t, "", stderr.String()) + + fm, err := os.Lstat(filepath.Join(dataDir, "extensions", "gh-local")) + require.NoErrorf(t, err, "data directory missing local extension symlink") + isSymlink := fm.Mode() & os.ModeSymlink + require.True(t, isSymlink != 0) + require.NoDirExistsf(t, filepath.Join(updateDir, "gh-local"), "update directory should be removed") +} diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index af82646396d..8662a2cd750 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -751,30 +751,6 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) { gcOne.AssertExpectations(t) } -func TestManager_InstallLocal(t *testing.T) { - dataDir := t.TempDir() - updateDir := t.TempDir() - - extDir := filepath.Join(t.TempDir(), "gh-local") - err := os.MkdirAll(extDir, 0755) - assert.NoErrorf(t, err, "failed to create local extension") - assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-local"))) - - ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(dataDir, updateDir, nil, nil, ios) - - err = m.InstallLocal(extDir) - assert.NoError(t, err) - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "", stderr.String()) - - fm, err := os.Lstat(filepath.Join(dataDir, "extensions", "gh-local")) - assert.NoErrorf(t, err, "data directory missing local extension symlink") - isSymlink := fm.Mode() & os.ModeSymlink - assert.True(t, isSymlink != 0) - assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-local"), "update directory should be removed") -} - func TestManager_Install_git(t *testing.T) { dataDir := t.TempDir() updateDir := t.TempDir() diff --git a/pkg/cmd/extension/manager_windows_test.go b/pkg/cmd/extension/manager_windows_test.go new file mode 100644 index 00000000000..f9142f4fcb9 --- /dev/null +++ b/pkg/cmd/extension/manager_windows_test.go @@ -0,0 +1,38 @@ +package extension + +import ( + "os" + "path/filepath" + "testing" + + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/require" +) + +func TestManager_InstallLocal_Windows(t *testing.T) { + dataDir := t.TempDir() + updateDir := t.TempDir() + + extDir := filepath.Join(t.TempDir(), "gh-local") + err := os.MkdirAll(extDir, 0755) + require.NoErrorf(t, err, "failed to create local extension") + require.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-local"))) + + ios, _, stdout, stderr := iostreams.Test() + m := newTestManager(dataDir, updateDir, nil, nil, ios) + + err = m.InstallLocal(extDir) + require.NoError(t, err) + require.Equal(t, "", stdout.String()) + require.Equal(t, "", stderr.String()) + + extDataFile := filepath.Join(dataDir, "extensions", "gh-local") + fm, err := os.Stat(extDataFile) + require.NoErrorf(t, err, "data directory missing Windows local extension file") + isSymlink := fm.Mode() & os.ModeSymlink + require.True(t, isSymlink == 0) + extDataFilePath, err := readPathFromFile(extDataFile) + require.NoErrorf(t, err, "failed to read Windows local extension path file") + require.Equal(t, extDir, extDataFilePath) + require.NoDirExistsf(t, filepath.Join(updateDir, "gh-local"), "update directory should be removed") +} From 56841d9cee9ec2c16230a35b30956e30f963fc34 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 11 Dec 2024 22:25:08 -0500 Subject: [PATCH 0029/2338] Add build constraint for non-Windows systems --- pkg/cmd/extension/manager_other_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/extension/manager_other_test.go b/pkg/cmd/extension/manager_other_test.go index 48ccb349bf5..fb478f1dea2 100644 --- a/pkg/cmd/extension/manager_other_test.go +++ b/pkg/cmd/extension/manager_other_test.go @@ -1,3 +1,4 @@ +//go:build !windows package extension import ( From c8501d82f25b59762ddddb99885540ff71200749 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 11 Dec 2024 22:27:59 -0500 Subject: [PATCH 0030/2338] Add build constraint for non-Windows systems --- pkg/cmd/extension/manager_other_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/extension/manager_other_test.go b/pkg/cmd/extension/manager_other_test.go index fb478f1dea2..416545c4b41 100644 --- a/pkg/cmd/extension/manager_other_test.go +++ b/pkg/cmd/extension/manager_other_test.go @@ -1,4 +1,5 @@ //go:build !windows + package extension import ( From c12e3694e85d7d6fc1d7269ab0cfc37dc6d38b4c Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 13 Dec 2024 22:52:03 -0500 Subject: [PATCH 0031/2338] Implement partial feedback before trunk update --- internal/ghcmd/cmd.go | 6 +++++- internal/update/update.go | 13 ++----------- pkg/cmd/extension/manager.go | 21 ++++++++++++--------- pkg/cmd/extension/manager_test.go | 2 +- pkg/cmd/root/extension.go | 12 ++++++------ 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/internal/ghcmd/cmd.go b/internal/ghcmd/cmd.go index a00dd760644..5586b51c3e8 100644 --- a/internal/ghcmd/cmd.go +++ b/internal/ghcmd/cmd.go @@ -29,6 +29,10 @@ import ( "github.com/spf13/cobra" ) +// updaterEnabled is a statically linked build property set in gh formula within homebrew/homebrew-core +// used to control whether users are notified of newer GitHub CLI releases. +// Development builds leave this empty as impossible to determine if newer or not. +// For more information, . var updaterEnabled = "" type exitCode int @@ -216,7 +220,7 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { } func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) { - if !update.ShouldCheckForUpdate(updaterEnabled) { + if updaterEnabled == "" || !update.ShouldCheckForUpdate() { return nil, nil } httpClient, err := f.HttpClient() diff --git a/internal/update/update.go b/internal/update/update.go index f9a97917a4c..30a4b41de25 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -33,7 +33,7 @@ type StateEntry struct { LatestRelease ReleaseInfo `yaml:"latest_release"` } -func ShouldCheckForExtensionUpdate() bool { +func ShouldCheckForUpdate() bool { if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { return false } @@ -44,6 +44,7 @@ func ShouldCheckForExtensionUpdate() bool { } func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, now time.Time) (*ReleaseInfo, error) { + // local extensions cannot have updates, so avoid work that ultimately returns nothing. if ext.IsLocal() { return nil, nil } @@ -71,16 +72,6 @@ func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte return nil, nil } -func ShouldCheckForUpdate(updaterEnabled string) bool { - if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { - return false - } - if os.Getenv("CODESPACES") != "" { - return false - } - return updaterEnabled != "" && !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) -} - // CheckForUpdate checks whether this software has had a newer release on GitHub func CheckForUpdate(ctx context.Context, client *http.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) { stateEntry, _ := getStateEntry(stateFilePath) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index ccb4b9bfa10..d7421d8e990 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -542,7 +542,7 @@ func (m *Manager) upgradeBinExtension(ext *Extension) error { } func (m *Manager) Remove(name string) error { - name = ensurePrefixed(name) + name = normalizeExtension(name) targetDir := filepath.Join(m.installDir(), name) if _, err := os.Lstat(targetDir); os.IsNotExist(err) { return fmt.Errorf("no extension found: %q", targetDir) @@ -562,7 +562,7 @@ func (m *Manager) installDir() string { // UpdateDir returns the extension-specific directory where updates are stored. func (m *Manager) UpdateDir(name string) string { - return filepath.Join(m.updateDir(), ensurePrefixed(name)) + return filepath.Join(m.updateDir(), normalizeExtension(name)) } //go:embed ext_tmpls/goBinMain.go.txt @@ -826,20 +826,23 @@ func codesignBinary(binPath string) error { return cmd.Run() } -// cleanExtensionUpdateDir should be used before installing extensions to avoid past metadata creating problems. +// cleanExtensionUpdateDir deletes extension state directory to avoid problems reinstalling extensions. func (m *Manager) cleanExtensionUpdateDir(name string) error { updatePath := m.UpdateDir(name) - if _, err := os.Stat(updatePath); err == nil { - if err := os.RemoveAll(updatePath); err != nil { - return fmt.Errorf("failed to remove previous extension update state: %w", err) + if _, err := os.Stat(updatePath); err != nil { + if os.IsNotExist(err) { + return nil } + return fmt.Errorf("failed to check previous extension update state: %w", err) + } + if err := os.RemoveAll(updatePath); err != nil { + return fmt.Errorf("failed to remove previous extension update state: %w", err) } - return nil } -// ensurePrefixed just makes sure that the provided extension name is prefixed with "gh-". -func ensurePrefixed(name string) string { +// normalizeExtension makes sure that the provided extension name is prefixed with "gh-". +func normalizeExtension(name string) string { if !strings.HasPrefix(name, "gh-") { name = "gh-" + name } diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 8662a2cd750..4258fd54e1e 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -1311,7 +1311,7 @@ func Test_ensurePrefixed(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, ensurePrefixed(tt.input)) + require.Equal(t, tt.expected, normalizeExtension(tt.input)) }) } } diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index 0eda58c2cd2..b9cb3dfff81 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -18,12 +18,12 @@ type ExternalCommandExitError struct { *exec.ExitError } -func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension, checkFunc func(extensions.ExtensionManager, extensions.Extension) (*update.ReleaseInfo, error)) *cobra.Command { +func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension, checkExtensionReleaseInfo func(extensions.ExtensionManager, extensions.Extension) (*update.ReleaseInfo, error)) *cobra.Command { updateMessageChan := make(chan *update.ReleaseInfo) cs := io.ColorScheme() hasDebug, _ := utils.IsDebugEnabled() - if checkFunc == nil { - checkFunc = checkForExtensionUpdate + if checkExtensionReleaseInfo == nil { + checkExtensionReleaseInfo = checkForExtensionUpdate } return &cobra.Command{ @@ -32,11 +32,11 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex // PreRun handles looking up whether extension has a latest version only when the command is ran. PreRun: func(c *cobra.Command, args []string) { go func() { - rel, err := checkFunc(em, ext) + releaseInfo, err := checkExtensionReleaseInfo(em, ext) if err != nil && hasDebug { fmt.Fprintf(io.ErrOut, "warning: checking for update failed: %v", err) } - updateMessageChan <- rel + updateMessageChan <- releaseInfo }() }, RunE: func(c *cobra.Command, args []string) error { @@ -77,7 +77,7 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex } func checkForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension) (*update.ReleaseInfo, error) { - if !update.ShouldCheckForExtensionUpdate() { + if !update.ShouldCheckForUpdate() { return nil, nil } From bfc63a14cc2cc0da8665387e65f2b0c6b55fe803 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 13 Dec 2024 23:09:18 -0500 Subject: [PATCH 0032/2338] Implement remaining PR feedback After merging in upstream changes regarding local extensions, this commit addresses remaining PR feedback while also bringing the newly merged tests into alignment with other changes. --- internal/update/update_test.go | 4 +- pkg/cmd/extension/manager_other_test.go | 36 ---------------- pkg/cmd/extension/manager_test.go | 51 ++++++++++++++++------- pkg/cmd/extension/manager_windows_test.go | 38 ----------------- pkg/cmd/root/extension_test.go | 8 ---- 5 files changed, 37 insertions(+), 100 deletions(-) delete mode 100644 pkg/cmd/extension/manager_other_test.go delete mode 100644 pkg/cmd/extension/manager_windows_test.go diff --git a/internal/update/update_test.go b/internal/update/update_test.go index e40a09c0c7f..ebddee810b1 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -368,7 +368,7 @@ func TestCheckForExtensionUpdate(t *testing.T) { } } -func TestShouldCheckForExtensionUpdate(t *testing.T) { +func TestShouldCheckForUpdate(t *testing.T) { tests := []struct { name string env map[string]string @@ -419,7 +419,7 @@ func TestShouldCheckForExtensionUpdate(t *testing.T) { os.Setenv(k, v) } - actual := ShouldCheckForExtensionUpdate() + actual := ShouldCheckForUpdate() require.Equal(t, tt.expected, actual) }) } diff --git a/pkg/cmd/extension/manager_other_test.go b/pkg/cmd/extension/manager_other_test.go deleted file mode 100644 index 416545c4b41..00000000000 --- a/pkg/cmd/extension/manager_other_test.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build !windows - -package extension - -import ( - "os" - "path/filepath" - "testing" - - "github.com/cli/cli/v2/pkg/iostreams" - "github.com/stretchr/testify/require" -) - -func TestManager_InstallLocal(t *testing.T) { - dataDir := t.TempDir() - updateDir := t.TempDir() - - extDir := filepath.Join(t.TempDir(), "gh-local") - err := os.MkdirAll(extDir, 0755) - require.NoErrorf(t, err, "failed to create local extension") - require.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-local"))) - - ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(dataDir, updateDir, nil, nil, ios) - - err = m.InstallLocal(extDir) - require.NoError(t, err) - require.Equal(t, "", stdout.String()) - require.Equal(t, "", stderr.String()) - - fm, err := os.Lstat(filepath.Join(dataDir, "extensions", "gh-local")) - require.NoErrorf(t, err, "data directory missing local extension symlink") - isSymlink := fm.Mode() & os.ModeSymlink - require.True(t, isSymlink != 0) - require.NoDirExistsf(t, filepath.Join(updateDir, "gh-local"), "update directory should be removed") -} diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index f8770eea570..53ea6b111f8 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -752,26 +752,31 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) { } func TestManager_Install_local(t *testing.T) { - extManagerDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(extManagerDir, nil, nil, ios) + m := newTestManager(dataDir, updateDir, nil, nil, ios) fakeExtensionName := "local-ext" // Create a temporary directory to simulate the local extension repo - extensionLocalPath := filepath.Join(extManagerDir, fakeExtensionName) + extensionLocalPath := filepath.Join(dataDir, fakeExtensionName) require.NoError(t, os.MkdirAll(extensionLocalPath, 0755)) // Create a fake executable in the local extension directory fakeExtensionExecutablePath := filepath.Join(extensionLocalPath, fakeExtensionName) require.NoError(t, stubExtension(fakeExtensionExecutablePath)) + // Create a temporary directory to simulate the local extension update state + extensionUpdatePath := filepath.Join(updateDir, fakeExtensionName) + require.NoError(t, stubExtensionUpdate(extensionUpdatePath)) + err := m.InstallLocal(extensionLocalPath) require.NoError(t, err) // This is the path to a file: // on windows this is a file whose contents is a string describing the path to the local extension dir. // on other platforms this file is a real symlink to the local extension dir. - extensionLinkFile := filepath.Join(extManagerDir, "extensions", fakeExtensionName) + extensionLinkFile := filepath.Join(dataDir, "extensions", fakeExtensionName) if runtime.GOOS == "windows" { // We don't create true symlinks on Windows, so check if we made a @@ -787,18 +792,24 @@ func TestManager_Install_local(t *testing.T) { } assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) + require.NoDirExistsf(t, extensionUpdatePath, "update directory should be removed") } func TestManager_Install_local_no_executable_found(t *testing.T) { - tempDir := t.TempDir() + dataDir := t.TempDir() + updateDir := t.TempDir() ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, nil, nil, ios) + m := newTestManager(dataDir, updateDir, nil, nil, ios) fakeExtensionName := "local-ext" // Create a temporary directory to simulate the local extension repo - localDir := filepath.Join(tempDir, fakeExtensionName) + localDir := filepath.Join(dataDir, fakeExtensionName) require.NoError(t, os.MkdirAll(localDir, 0755)) + // Create a temporary directory to simulate the local extension update state + extensionUpdatePath := filepath.Join(updateDir, fakeExtensionName) + require.NoError(t, stubExtensionUpdate(extensionUpdatePath)) + // Intentionally not creating an executable in the local extension repo // to simulate an attempt to install a local extension without an executable @@ -806,6 +817,7 @@ func TestManager_Install_local_no_executable_found(t *testing.T) { require.ErrorAs(t, err, new(*ErrExtensionExecutableNotFound)) assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) + require.NoDirExistsf(t, extensionUpdatePath, "update directory should be removed") } func TestManager_Install_git(t *testing.T) { @@ -818,10 +830,14 @@ func TestManager_Install_git(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() - extensionDir := filepath.Join(dataDir, "extensions", "gh-some-ext") + fakeExtensionName := "gh-some-ext" + extensionDir := filepath.Join(dataDir, "extensions", fakeExtensionName) gc := &mockGitClient{} gc.On("Clone", "https://github.com/owner/gh-some-ext.git", []string{extensionDir}).Return("", nil).Once() - assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-some-ext"))) + + // Create a temporary directory to simulate the local extension update state + extensionUpdatePath := filepath.Join(updateDir, fakeExtensionName) + require.NoError(t, stubExtensionUpdate(extensionUpdatePath)) m := newTestManager(dataDir, updateDir, &client, gc, ios) @@ -840,7 +856,7 @@ func TestManager_Install_git(t *testing.T) { httpmock.REST("GET", "repos/owner/gh-some-ext/contents/gh-some-ext"), httpmock.StringResponse("script")) - repo := ghrepo.New("owner", "gh-some-ext") + repo := ghrepo.New("owner", fakeExtensionName) err := m.Install(repo, "") assert.NoError(t, err) @@ -848,7 +864,7 @@ func TestManager_Install_git(t *testing.T) { assert.Equal(t, "", stderr.String()) gc.AssertExpectations(t) - assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-some-ext"), "update directory should be removed") + assert.NoDirExistsf(t, extensionUpdatePath, "update directory should be removed") } func TestManager_Install_git_pinned(t *testing.T) { @@ -1062,7 +1078,8 @@ func TestManager_Install_rosetta_fallback_not_found(t *testing.T) { } func TestManager_Install_binary(t *testing.T) { - repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") + fakeExtensionName := "gh-bin-ext" + repo := ghrepo.NewWithHost("owner", fakeExtensionName, "example.com") reg := httpmock.Registry{} defer reg.Verify(t) @@ -1097,7 +1114,10 @@ func TestManager_Install_binary(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() dataDir := t.TempDir() updateDir := t.TempDir() - assert.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-bin-ext"))) + + // Create a temporary directory to simulate the local extension update state + extensionUpdatePath := filepath.Join(updateDir, fakeExtensionName) + require.NoError(t, stubExtensionUpdate(extensionUpdatePath)) m := newTestManager(dataDir, updateDir, &http.Client{Transport: ®}, nil, ios) @@ -1112,7 +1132,7 @@ func TestManager_Install_binary(t *testing.T) { assert.NoError(t, err) assert.Equal(t, binManifest{ - Name: "gh-bin-ext", + Name: fakeExtensionName, Owner: "owner", Host: "example.com", Tag: "v1.0.1", @@ -1125,8 +1145,7 @@ func TestManager_Install_binary(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - - assert.NoDirExistsf(t, filepath.Join(updateDir, "gh-bin-ext"), "update directory should be removed") + require.NoDirExistsf(t, extensionUpdatePath, "update directory should be removed") } func TestManager_Install_amd64_when_supported(t *testing.T) { diff --git a/pkg/cmd/extension/manager_windows_test.go b/pkg/cmd/extension/manager_windows_test.go deleted file mode 100644 index f9142f4fcb9..00000000000 --- a/pkg/cmd/extension/manager_windows_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package extension - -import ( - "os" - "path/filepath" - "testing" - - "github.com/cli/cli/v2/pkg/iostreams" - "github.com/stretchr/testify/require" -) - -func TestManager_InstallLocal_Windows(t *testing.T) { - dataDir := t.TempDir() - updateDir := t.TempDir() - - extDir := filepath.Join(t.TempDir(), "gh-local") - err := os.MkdirAll(extDir, 0755) - require.NoErrorf(t, err, "failed to create local extension") - require.NoError(t, stubExtensionUpdate(filepath.Join(updateDir, "gh-local"))) - - ios, _, stdout, stderr := iostreams.Test() - m := newTestManager(dataDir, updateDir, nil, nil, ios) - - err = m.InstallLocal(extDir) - require.NoError(t, err) - require.Equal(t, "", stdout.String()) - require.Equal(t, "", stderr.String()) - - extDataFile := filepath.Join(dataDir, "extensions", "gh-local") - fm, err := os.Stat(extDataFile) - require.NoErrorf(t, err, "data directory missing Windows local extension file") - isSymlink := fm.Mode() & os.ModeSymlink - require.True(t, isSymlink == 0) - extDataFilePath, err := readPathFromFile(extDataFile) - require.NoErrorf(t, err, "failed to read Windows local extension path file") - require.Equal(t, extDir, extDataFilePath) - require.NoDirExistsf(t, filepath.Join(updateDir, "gh-local"), "update directory should be removed") -} diff --git a/pkg/cmd/root/extension_test.go b/pkg/cmd/root/extension_test.go index 1d74c129771..32b250de585 100644 --- a/pkg/cmd/root/extension_test.go +++ b/pkg/cmd/root/extension_test.go @@ -17,7 +17,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { tests := []struct { name string extCurrentVersion string - extFullName string extIsPinned bool extLatestVersion string extName string @@ -28,7 +27,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "no update available", extName: "no-update", - extFullName: "gh-no-update", extUpdateAvailable: false, extCurrentVersion: "1.0.0", extLatestVersion: "1.0.0", @@ -37,7 +35,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "major update", extName: "major-update", - extFullName: "gh-major-update", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "2.0.0", @@ -51,7 +48,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "major update, pinned", extName: "major-update-pin", - extFullName: "gh-major-update-pin", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "2.0.0", @@ -66,7 +62,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "minor update", extName: "minor-update", - extFullName: "gh-minor-update", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.1.0", @@ -80,7 +75,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "minor update, pinned", extName: "minor-update-pin", - extFullName: "gh-minor-update-pin", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.1.0", @@ -95,7 +89,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "patch update", extName: "patch-update", - extFullName: "gh-patch-update", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.0.1", @@ -109,7 +102,6 @@ func TestNewCmdExtension_Updates(t *testing.T) { { name: "patch update, pinned", extName: "patch-update-pin", - extFullName: "gh-patch-update-pin", extUpdateAvailable: true, extCurrentVersion: "1.0.0", extLatestVersion: "1.0.1", From edc0351c85c429af8ce404e7df504fafe076e9e9 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 13 Dec 2024 23:20:42 -0500 Subject: [PATCH 0033/2338] Fix failig local extension tests Extensions have to be gh- prefixed and these newer tests were failing because of expectation. --- pkg/cmd/extension/manager_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 53ea6b111f8..67b4ad1cc1f 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -756,7 +756,7 @@ func TestManager_Install_local(t *testing.T) { updateDir := t.TempDir() ios, _, stdout, stderr := iostreams.Test() m := newTestManager(dataDir, updateDir, nil, nil, ios) - fakeExtensionName := "local-ext" + fakeExtensionName := "gh-local-ext" // Create a temporary directory to simulate the local extension repo extensionLocalPath := filepath.Join(dataDir, fakeExtensionName) @@ -800,7 +800,7 @@ func TestManager_Install_local_no_executable_found(t *testing.T) { updateDir := t.TempDir() ios, _, stdout, stderr := iostreams.Test() m := newTestManager(dataDir, updateDir, nil, nil, ios) - fakeExtensionName := "local-ext" + fakeExtensionName := "gh-local-ext" // Create a temporary directory to simulate the local extension repo localDir := filepath.Join(dataDir, fakeExtensionName) From 1f525bf1752cb171150f2c899a2d228943f98138 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 15 Dec 2024 12:34:46 +1100 Subject: [PATCH 0034/2338] issue #2329: handle empty list of PR --- pkg/cmd/pr/checkout/checkout.go | 4 +++- pkg/cmd/pr/checkout/checkout_test.go | 27 ++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 88850b940f0..8df4a6faffe 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -333,9 +333,11 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar if err != nil { return nil, err } + if len(listResult.PullRequests) == 0 { + return nil, shared.ListNoResults(ghrepo.FullName(baseRepo), "pull request", false) + } pr, err := promptForPR(prompter, *listResult) - return pr, err } diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index f2b37991233..b1d70f68bd0 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -75,6 +75,7 @@ func Test_checkoutRun(t *testing.T) { wantStdout string wantStderr string wantErr bool + errMsg string }{ { name: "checkout with ssh remote URL", @@ -204,7 +205,7 @@ func Test_checkoutRun(t *testing.T) { }, }, { - name: "with no selected PR args and not stdin tty, return error", + name: "with no selected PR args and non tty, return error", opts: &CheckoutOptions{ SelectorArg: "", Interactive: false, @@ -216,6 +217,7 @@ func Test_checkoutRun(t *testing.T) { "origin": "OWNER/REPO", }, wantErr: true, + errMsg: "pull request number, URL, or branch required when not running interactively", }, { name: "with no selected PR args and stdin tty, prompts for choice", @@ -253,6 +255,26 @@ func Test_checkoutRun(t *testing.T) { "origin": "OWNER/REPO", }, }, + { + name: "with no select PR args and no open PR, return error", + opts: &CheckoutOptions{ + SelectorArg: "", + Interactive: true, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.GraphQL(`query PullRequestList\b`), + httpmock.StringResponse(`{"data":{"repository":{"pullRequests":{ "totalCount": 0,"nodes":[]}}}}`)) + + }, + remotes: map[string]string{ + "origin": "OWNER/REPO", + }, + wantErr: true, + errMsg: "no open pull requests in OWNER/REPO", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -309,6 +331,9 @@ func Test_checkoutRun(t *testing.T) { if (err != nil) != tt.wantErr { t.Errorf("want error: %v, got: %v", tt.wantErr, err) } + if err != nil { + assert.Equal(t, tt.errMsg, err.Error()) + } assert.Equal(t, tt.wantStdout, stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) }) From d7cabf18f7b805dec117f641e071c23247f64872 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 15 Dec 2024 13:55:47 +1100 Subject: [PATCH 0035/2338] fix: tab format may break checkout prompts --- pkg/cmd/pr/checkout/checkout.go | 2 +- pkg/cmd/pr/checkout/checkout_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 8df4a6faffe..faf006c7e29 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -344,7 +344,7 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar func promptForPR(prompter shared.Prompter, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { candidates := []string{} for _, pr := range jobs.PullRequests { - candidates = append(candidates, fmt.Sprintf("%d\t%s\t%s [%s]", + candidates = append(candidates, fmt.Sprintf("%d\t%s %s [%s]", pr.Number, shared.PrStateWithDraft(&pr), text.RemoveExcessiveWhitespace(pr.Title), diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index b1d70f68bd0..a099a848d28 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -241,9 +241,9 @@ func Test_checkoutRun(t *testing.T) { }, promptStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Select a pull request", - []string{"32\tDRAFT\tNew feature [feature]", "29\tOPEN\tFixed bad bug [bug-fix]", "28\tOPEN\tImprove documentation [docs]"}, + []string{"32\tDRAFT New feature [feature]", "29\tOPEN Fixed bad bug [bug-fix]", "28\tOPEN Improve documentation [docs]"}, func(_, _ string, opts []string) (int, error) { - return prompter.IndexFor(opts, "32\tDRAFT\tNew feature [feature]") + return prompter.IndexFor(opts, "32\tDRAFT New feature [feature]") }) }, runStubs: func(cs *run.CommandStubber) { From 91b3b99b767dd0367a49d87668885d83c43515bd Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 15 Dec 2024 14:01:08 +1100 Subject: [PATCH 0036/2338] issue #2329: create shared PRLister --- pkg/cmd/pr/checkout/checkout.go | 41 +++--- pkg/cmd/pr/checkout/checkout_test.go | 41 ++++-- pkg/cmd/pr/shared/finder.go | 4 +- pkg/cmd/pr/shared/lister.go | 196 +++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 39 deletions(-) create mode 100644 pkg/cmd/pr/shared/lister.go diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index faf006c7e29..b265ea25eab 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -13,7 +13,6 @@ import ( "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" - "github.com/cli/cli/v2/pkg/cmd/pr/list" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -30,6 +29,7 @@ type CheckoutOptions struct { Finder shared.PRFinder Prompter shared.Prompter + Lister shared.PRLister Interactive bool BaseRepo func() (ghrepo.Interface, error) @@ -67,6 +67,7 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Finder = shared.NewFinder(f) + opts.Lister = shared.NewLister(f) if len(args) > 0 { opts.SelectorArg = args[0] @@ -97,12 +98,7 @@ func checkoutRun(opts *CheckoutOptions) error { return err } - client, err := opts.HttpClient() - if err != nil { - return err - } - - pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.IO) + pr, err := resolvePR(baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.Lister, opts.IO) if err != nil { return err } @@ -291,7 +287,7 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm return nil } -func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, io *iostreams.IOStreams) (*api.PullRequest, error) { +func resolvePR(baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, prLister shared.PRLister, io *iostreams.IOStreams) (*api.PullRequest, error) { // When non-interactive if pullRequestSelector != "" { pr, _, err := pullRequestFinder.Find(shared.FindOptions{ @@ -315,20 +311,21 @@ func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shar } // When interactive io.StartProgressIndicator() - listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{ - "number", - "title", - "state", - "url", - "isDraft", - "createdAt", - - "headRefName", - "headRepository", - "headRepositoryOwner", - "isCrossRepository", - "maintainerCanModify", - }}, 10) + listResult, err := prLister.List(shared.ListOptions{ + State: "open", + Fields: []string{ + "number", + "title", + "state", + "isDraft", + + "headRefName", + "headRepository", + "headRepositoryOwner", + "isCrossRepository", + "maintainerCanModify", + }, + LimitResults: 10}) io.StopProgressIndicator() if err != nil { return nil, err diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index a099a848d28..bd8bc3984ac 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -28,6 +28,10 @@ import ( // repo: either "baseOwner/baseRepo" or "baseOwner/baseRepo:defaultBranch" // prHead: "headOwner/headRepo:headBranch" func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) { + return _stubPR(repo, prHead, 123, "PR title", "OPEN", false) +} + +func _stubPR(repo, prHead string, number int, title string, state string, isDraft bool) (ghrepo.Interface, *api.PullRequest) { defaultBranch := "" if idx := strings.IndexRune(repo, ':'); idx >= 0 { defaultBranch = repo[idx+1:] @@ -53,12 +57,16 @@ func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) { } return baseRepo, &api.PullRequest{ - Number: 123, + Number: number, HeadRefName: headRefName, HeadRepositoryOwner: api.Owner{Login: headRepo.RepoOwner()}, HeadRepository: &api.PRRepository{Name: headRepo.RepoName()}, IsCrossRepository: !ghrepo.IsSame(baseRepo, headRepo), MaintainerCanModify: false, + + Title: title, + State: state, + IsDraft: isDraft, } } @@ -224,10 +232,17 @@ func Test_checkoutRun(t *testing.T) { opts: &CheckoutOptions{ SelectorArg: "", Interactive: true, - Finder: func() shared.PRFinder { - baseRepo, pr := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") - finder := shared.NewMockFinder("123", pr, baseRepo) - return finder + Lister: func() shared.PRLister { + _, pr1 := _stubPR("OWNER/REPO:master", "OWNER/REPO:feature", 32, "New feature", "OPEN", false) + _, pr2 := _stubPR("OWNER/REPO:master", "OWNER/REPO:bug-fix", 29, "Fixed bad bug", "OPEN", false) + _, pr3 := _stubPR("OWNER/REPO:master", "OWNER/REPO:docs", 28, "Improve documentation", "OPEN", true) + lister := shared.NewMockLister(&api.PullRequestAndTotalCount{ + TotalCount: 3, + PullRequests: []api.PullRequest{ + *pr1, *pr2, *pr3, + }, SearchCapped: false}, nil) + lister.ExpectFields([]string{"number", "title", "state", "isDraft", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"}) + return lister }(), BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil @@ -236,14 +251,11 @@ func Test_checkoutRun(t *testing.T) { return config.NewBlankConfig(), nil }, }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) - }, promptStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Select a pull request", - []string{"32\tDRAFT New feature [feature]", "29\tOPEN Fixed bad bug [bug-fix]", "28\tOPEN Improve documentation [docs]"}, + []string{"32\tOPEN New feature [feature]", "29\tOPEN Fixed bad bug [bug-fix]", "28\tDRAFT Improve documentation [docs]"}, func(_, _ string, opts []string) (int, error) { - return prompter.IndexFor(opts, "32\tDRAFT New feature [feature]") + return prompter.IndexFor(opts, "32\tOPEN New feature [feature]") }) }, runStubs: func(cs *run.CommandStubber) { @@ -263,11 +275,10 @@ func Test_checkoutRun(t *testing.T) { BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.GraphQL(`query PullRequestList\b`), - httpmock.StringResponse(`{"data":{"repository":{"pullRequests":{ "totalCount": 0,"nodes":[]}}}}`)) - + Lister: shared.NewMockLister(&api.PullRequestAndTotalCount{ + TotalCount: 0, + PullRequests: []api.PullRequest{}, + }, nil), }, remotes: map[string]string{ "origin": "OWNER/REPO", diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index e4a70eb2230..b1fec267ead 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -548,7 +548,7 @@ func (m *mockFinder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, if m.expectSelector != opts.Selector { return nil, nil, fmt.Errorf("mockFinder: expected selector %q, got %q", m.expectSelector, opts.Selector) } - if len(m.expectFields) > 0 && !isEqualSet(m.expectFields, opts.Fields) { + if len(m.expectFields) > 0 && !IsEqualSet(m.expectFields, opts.Fields) { return nil, nil, fmt.Errorf("mockFinder: expected fields %v, got %v", m.expectFields, opts.Fields) } if m.called { @@ -568,7 +568,7 @@ func (m *mockFinder) ExpectFields(fields []string) { m.expectFields = fields } -func isEqualSet(a, b []string) bool { +func IsEqualSet(a, b []string) bool { if len(a) != len(b) { return false } diff --git a/pkg/cmd/pr/shared/lister.go b/pkg/cmd/pr/shared/lister.go new file mode 100644 index 00000000000..004f726d572 --- /dev/null +++ b/pkg/cmd/pr/shared/lister.go @@ -0,0 +1,196 @@ +package shared + +import ( + "fmt" + "net/http" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + + api "github.com/cli/cli/v2/api" +) + +type PRLister interface { + List(opt ListOptions) (*api.PullRequestAndTotalCount, error) +} + +type ListOptions struct { + LimitResults int + + State string + BaseBranch string + HeadBranch string + + Fields []string +} + +type lister struct { + baseRepoFn func() (ghrepo.Interface, error) + httpClient func() (*http.Client, error) +} + +func NewLister(factory *cmdutil.Factory) PRLister { + return &lister{ + baseRepoFn: factory.BaseRepo, + httpClient: factory.HttpClient, + } +} + +func (l *lister) List(opts ListOptions) (*api.PullRequestAndTotalCount, error) { + repo, err := l.baseRepoFn() + if err != nil { + return nil, err + } + + client, err := l.httpClient() + if err != nil { + return nil, err + } + + return listPullRequests(client, repo, opts) +} + +func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters ListOptions) (*api.PullRequestAndTotalCount, error) { + type response struct { + Repository struct { + PullRequests struct { + Nodes []api.PullRequest + PageInfo struct { + HasNextPage bool + EndCursor string + } + TotalCount int + } + } + } + limit := filters.LimitResults + fragment := fmt.Sprintf("fragment pr on PullRequest{%s}", api.PullRequestGraphQL(filters.Fields)) + query := fragment + ` + query PullRequestList( + $owner: String!, + $repo: String!, + $limit: Int!, + $endCursor: String, + $baseBranch: String, + $headBranch: String, + $state: [PullRequestState!] = OPEN + ) { + repository(owner: $owner, name: $repo) { + pullRequests( + states: $state, + baseRefName: $baseBranch, + headRefName: $headBranch, + first: $limit, + after: $endCursor, + orderBy: {field: CREATED_AT, direction: DESC} + ) { + totalCount + nodes { + ...pr + } + pageInfo { + hasNextPage + endCursor + } + } + } + }` + + pageLimit := min(limit, 100) + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "repo": repo.RepoName(), + } + + switch filters.State { + case "open": + variables["state"] = []string{"OPEN"} + case "closed": + variables["state"] = []string{"CLOSED", "MERGED"} + case "merged": + variables["state"] = []string{"MERGED"} + case "all": + variables["state"] = []string{"OPEN", "CLOSED", "MERGED"} + default: + return nil, fmt.Errorf("invalid state: %s", filters.State) + } + + if filters.BaseBranch != "" { + variables["baseBranch"] = filters.BaseBranch + } + if filters.HeadBranch != "" { + variables["headBranch"] = filters.HeadBranch + } + + res := api.PullRequestAndTotalCount{} + var check = make(map[int]struct{}) + client := api.NewClientFromHTTP(httpClient) + +loop: + for { + variables["limit"] = pageLimit + var data response + err := client.GraphQL(repo.RepoHost(), query, variables, &data) + if err != nil { + return nil, err + } + prData := data.Repository.PullRequests + res.TotalCount = prData.TotalCount + + for _, pr := range prData.Nodes { + if _, exists := check[pr.Number]; exists && pr.Number > 0 { + continue + } + check[pr.Number] = struct{}{} + + res.PullRequests = append(res.PullRequests, pr) + if len(res.PullRequests) == limit { + break loop + } + } + + if prData.PageInfo.HasNextPage { + variables["endCursor"] = prData.PageInfo.EndCursor + pageLimit = min(pageLimit, limit-len(res.PullRequests)) + } else { + break + } + } + + return &res, nil +} + +type mockLister struct { + called bool + expectFields []string + + result *api.PullRequestAndTotalCount + err error +} + +func NewMockLister(result *api.PullRequestAndTotalCount, err error) *mockLister { + return &mockLister{ + result: result, + err: err, + } +} + +func (m *mockLister) List(opt ListOptions) (*api.PullRequestAndTotalCount, error) { + m.called = true + if m.err != nil { + return nil, m.err + } + + if m.expectFields != nil { + + if !IsEqualSet(m.expectFields, opt.Fields) { + return nil, fmt.Errorf("unexpected fields: %v", opt.Fields) + } + } + + return m.result, m.err +} + +func (m *mockLister) ExpectFields(fields []string) { + m.expectFields = fields +} From 55598d0a2c7d0b4c4772d3c824b6339d4dd90bc2 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 15 Dec 2024 18:03:06 +1100 Subject: [PATCH 0037/2338] feat: integrate new PRLister into pr list command --- pkg/cmd/pr/list/http.go | 120 ++++------------------------------- pkg/cmd/pr/list/http_test.go | 2 +- pkg/cmd/pr/list/list.go | 2 +- pkg/cmd/pr/list/list_test.go | 4 +- 4 files changed, 16 insertions(+), 112 deletions(-) diff --git a/pkg/cmd/pr/list/http.go b/pkg/cmd/pr/list/http.go index cb79b3b87db..3836b9eb929 100644 --- a/pkg/cmd/pr/list/http.go +++ b/pkg/cmd/pr/list/http.go @@ -7,124 +7,28 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" + "github.com/cli/cli/v2/pkg/cmdutil" ) func shouldUseSearch(filters prShared.FilterOptions) bool { return filters.Draft != nil || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0 } -func ListPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { +func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { if shouldUseSearch(filters) { return searchPullRequests(httpClient, repo, filters, limit) } - type response struct { - Repository struct { - PullRequests struct { - Nodes []api.PullRequest - PageInfo struct { - HasNextPage bool - EndCursor string - } - TotalCount int - } - } - } - - fragment := fmt.Sprintf("fragment pr on PullRequest{%s}", api.PullRequestGraphQL(filters.Fields)) - query := fragment + ` - query PullRequestList( - $owner: String!, - $repo: String!, - $limit: Int!, - $endCursor: String, - $baseBranch: String, - $headBranch: String, - $state: [PullRequestState!] = OPEN - ) { - repository(owner: $owner, name: $repo) { - pullRequests( - states: $state, - baseRefName: $baseBranch, - headRefName: $headBranch, - first: $limit, - after: $endCursor, - orderBy: {field: CREATED_AT, direction: DESC} - ) { - totalCount - nodes { - ...pr - } - pageInfo { - hasNextPage - endCursor - } - } - } - }` - - pageLimit := min(limit, 100) - variables := map[string]interface{}{ - "owner": repo.RepoOwner(), - "repo": repo.RepoName(), - } - - switch filters.State { - case "open": - variables["state"] = []string{"OPEN"} - case "closed": - variables["state"] = []string{"CLOSED", "MERGED"} - case "merged": - variables["state"] = []string{"MERGED"} - case "all": - variables["state"] = []string{"OPEN", "CLOSED", "MERGED"} - default: - return nil, fmt.Errorf("invalid state: %s", filters.State) - } - - if filters.BaseBranch != "" { - variables["baseBranch"] = filters.BaseBranch - } - if filters.HeadBranch != "" { - variables["headBranch"] = filters.HeadBranch - } - - res := api.PullRequestAndTotalCount{} - var check = make(map[int]struct{}) - client := api.NewClientFromHTTP(httpClient) - -loop: - for { - variables["limit"] = pageLimit - var data response - err := client.GraphQL(repo.RepoHost(), query, variables, &data) - if err != nil { - return nil, err - } - prData := data.Repository.PullRequests - res.TotalCount = prData.TotalCount - - for _, pr := range prData.Nodes { - if _, exists := check[pr.Number]; exists && pr.Number > 0 { - continue - } - check[pr.Number] = struct{}{} - - res.PullRequests = append(res.PullRequests, pr) - if len(res.PullRequests) == limit { - break loop - } - } - - if prData.PageInfo.HasNextPage { - variables["endCursor"] = prData.PageInfo.EndCursor - pageLimit = min(pageLimit, limit-len(res.PullRequests)) - } else { - break - } - } - - return &res, nil + return prShared.NewLister(&cmdutil.Factory{ + HttpClient: func() (*http.Client, error) { return httpClient, nil }, + BaseRepo: func() (ghrepo.Interface, error) { return repo, nil }, + }).List(prShared.ListOptions{ + LimitResults: limit, + State: filters.State, + BaseBranch: filters.BaseBranch, + HeadBranch: filters.HeadBranch, + Fields: filters.Fields, + }) } func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { diff --git a/pkg/cmd/pr/list/http_test.go b/pkg/cmd/pr/list/http_test.go index 060e3a17924..1aa16ae1d8a 100644 --- a/pkg/cmd/pr/list/http_test.go +++ b/pkg/cmd/pr/list/http_test.go @@ -153,7 +153,7 @@ func Test_ListPullRequests(t *testing.T) { } httpClient := &http.Client{Transport: reg} - _, err := ListPullRequests(httpClient, tt.args.repo, tt.args.filters, tt.args.limit) + _, err := listPullRequests(httpClient, tt.args.repo, tt.args.filters, tt.args.limit) if (err != nil) != tt.wantErr { t.Errorf("ListPullRequests() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index e5ebe50fb96..b160f7ebcd2 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -172,7 +172,7 @@ func listRun(opts *ListOptions) error { return opts.Browser.Browse(openURL) } - listResult, err := ListPullRequests(httpClient, baseRepo, filters, opts.LimitResults) + listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults) if err != nil { return err } diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go index 7ee111afc1c..ecd0326b508 100644 --- a/pkg/cmd/pr/list/list_test.go +++ b/pkg/cmd/pr/list/list_test.go @@ -368,7 +368,7 @@ func TestPRList_withProjectItems(t *testing.T) { })) client := &http.Client{Transport: reg} - prsAndTotalCount, err := ListPullRequests( + prsAndTotalCount, err := listPullRequests( client, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{ @@ -437,7 +437,7 @@ func TestPRList_Search_withProjectItems(t *testing.T) { })) client := &http.Client{Transport: reg} - prsAndTotalCount, err := ListPullRequests( + prsAndTotalCount, err := listPullRequests( client, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{ From 3987f4af4a3eb7d4d289ceacfb82a68bb9548da8 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 15 Dec 2024 18:08:45 +1100 Subject: [PATCH 0038/2338] chore: remove unused file --- pkg/cmd/pr/checkout/fixtures/prList.json | 66 ------------------------ 1 file changed, 66 deletions(-) delete mode 100644 pkg/cmd/pr/checkout/fixtures/prList.json diff --git a/pkg/cmd/pr/checkout/fixtures/prList.json b/pkg/cmd/pr/checkout/fixtures/prList.json deleted file mode 100644 index 84928722c53..00000000000 --- a/pkg/cmd/pr/checkout/fixtures/prList.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "data": { - "repository": { - "pullRequests": { - "totalCount": 3, - "nodes": [ - { - "number": 32, - "title": "New feature", - "url": "https://github.com/OWNER/REPO/pull/32", - "createdAt": "2022-08-24T20:01:12Z", - "headRefName": "feature", - "state": "OPEN", - "isDraft": true, - "isCrossRepository": false, - "headRepository": { - "name": "REPO" - }, - "headRepositoryOwner": { - "login": "OWNER" - }, - "maintainerCanModify": true - }, - { - "number": 29, - "title": "Fixed bad bug", - "url": "https://github.com/OWNER/REPO/pull/29", - "createdAt": "2022-07-20T19:01:12Z", - "headRefName": "bug-fix", - "state": "OPEN", - "isDraft": false, - "isCrossRepository": false, - "headRepository": { - "name": "REPO" - }, - "headRepositoryOwner": { - "login": "OWNER" - }, - "maintainerCanModify": true - }, - { - "number": 28, - "state": "OPEN", - "isDraft": false, - "title": "Improve documentation", - "createdAt": "2020-01-26T19:01:12Z", - "url": "https://github.com/OWNER/REPO/pull/28", - "isCrossRepository": false, - "headRefName": "docs", - "headRepository": { - "name": "REPO" - }, - "headRepositoryOwner": { - "login": "OWNER" - }, - "maintainerCanModify": true - } - ], - "pageInfo": { - "hasNextPage": false, - "endCursor": "" - } - } - } - } -} From 5a6a7968a30b70e50dbc8521f2bcaf5d23c6102b Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 11:25:43 -0700 Subject: [PATCH 0039/2338] fetch bundles with sas url Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/attestation.go | 4 +- pkg/cmd/attestation/api/client.go | 83 +++++++++++++------ pkg/cmd/attestation/api/mock_client.go | 7 +- .../attestation/verification/attestation.go | 20 ++--- 4 files changed, 69 insertions(+), 45 deletions(-) diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go index d5c444ee297..16b062a9689 100644 --- a/pkg/cmd/attestation/api/attestation.go +++ b/pkg/cmd/attestation/api/attestation.go @@ -25,8 +25,8 @@ func newErrNoAttestations(name, digest string) ErrNoAttestations { } type Attestation struct { - Bundle *bundle.Bundle `json:"bundle"` - SASUrl string `json:"signedAccessSignatureUrl"` + Bundle *bundle.Bundle `json:"bundle"` + BundleURL string `json:"bundle_url"` } type AttestationsResponse struct { diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index f2f9f48f865..acc4a101408 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -1,12 +1,10 @@ package api import ( - "encoding/json" "errors" "fmt" "io" "net/http" - "net/url" "strings" "time" @@ -16,6 +14,7 @@ import ( "github.com/golang/snappy" v1 "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" "github.com/sigstore/sigstore-go/pkg/bundle" + "google.golang.org/protobuf/encoding/protojson" ) const ( @@ -24,13 +23,16 @@ const ( maxLimitForFetch = 100 ) +// Allow injecting backoff interval in tests. +var getAttestationRetryInterval = time.Millisecond * 200 + type apiClient interface { REST(hostname, method, p string, body io.Reader, data interface{}) error RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) } type Client interface { - FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) + // FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) GetTrustDomain() (string, error) @@ -58,7 +60,18 @@ func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { // GetByRepoAndDigest fetches the attestation by repo and digest func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { url := c.BuildRepoAndDigestURL(repo, digest) - return c.getAttestations(url, repo, digest, limit) + attestations, err := c.getAttestations(url, repo, digest, limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) + } + + compressedBundles, err := c.fetchBundlesWithSASURL(attestations) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + } + + // decompress before returning + return compressedBundles, nil } func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { @@ -69,7 +82,18 @@ func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { // GetByOwnerAndDigest fetches attestation by owner and digest func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { url := c.BuildOwnerAndDigestURL(owner, digest) - return c.getAttestations(url, owner, digest, limit) + attestations, err := c.getAttestations(url, owner, digest, limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) + } + + compressedBundles, err := c.fetchBundlesWithSASURL(attestations) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + } + + // decompress before returning + return compressedBundles, nil } // GetTrustDomain returns the current trust domain. If the default is used @@ -78,9 +102,6 @@ func (c *LiveClient) GetTrustDomain() (string, error) { return c.getTrustDomain(MetaPath) } -// Allow injecting backoff interval in tests. -var getAttestationRetryInterval = time.Millisecond * 200 - func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) { c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) @@ -136,12 +157,12 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At return attestations, nil } -func (c *LiveClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { +func (c *LiveClient) fetchBundlesWithSASURL(attestations []*Attestation) ([]*Attestation, error) { fetched := make([]*Attestation, len(attestations)) for i, a := range attestations { - b, err := c.fetchAttestationWithSASURL(a) + b, err := c.fetchBundleWithSASURL(a) if err != nil { - return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + return nil, fmt.Errorf("failed to fetch bundle with SAS URL: %w", err) } fetched[i] = &Attestation{ Bundle: b, @@ -150,33 +171,45 @@ func (c *LiveClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([ return fetched, nil } -func (c *LiveClient) fetchAttestationWithSASURL(a *Attestation) (*bundle.Bundle, error) { - if a.SASUrl == "" { - return a.Bundle, nil +func (c *LiveClient) fetchBundleWithSASURL(a *Attestation) (*bundle.Bundle, error) { + if a.BundleURL == "" { + //return a.Bundle, nil + return nil, fmt.Errorf("SAS URL is empty") } - parsed, err := url.Parse(a.SASUrl) + c.logger.VerbosePrintf("Fetching attestation bundle\n\n") + + httpClient := http.DefaultClient + r, err := httpClient.Get(a.BundleURL) if err != nil { return nil, err } - var resp []byte - if err = c.api.REST(parsed.Host, http.MethodGet, parsed.Path, nil, &resp); err != nil { - return nil, err + defer r.Body.Close() + if r.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", r.StatusCode) } - var decompressedBytes []byte - _, err = snappy.Decode(decompressedBytes, resp) + // parse the URL to get the host and path + resp, err := io.ReadAll(r.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read blob storage response body: %w", err) } - var pbBundle *v1.Bundle - if err = json.Unmarshal(decompressedBytes, pbBundle); err != nil { - return nil, err + var out []byte + decompressed, err := snappy.Decode(out, resp) + if err != nil { + return nil, fmt.Errorf("failed to decompress with snappy: %w", err) } - return bundle.NewBundle(pbBundle) + var pbBundle v1.Bundle + if err = protojson.Unmarshal(decompressed, &pbBundle); err != nil { + return nil, fmt.Errorf("failed to unmarshal to bundle: %w", err) + } + + c.logger.VerbosePrintf("Successfully fetched bundle\n\n") + + return bundle.NewBundle(&pbBundle) } func shouldRetry(err error) bool { diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go index 17bfdadeca6..63dd3d24422 100644 --- a/pkg/cmd/attestation/api/mock_client.go +++ b/pkg/cmd/attestation/api/mock_client.go @@ -7,9 +7,10 @@ import ( ) type MockClient struct { - OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) - OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) - OnGetTrustDomain func() (string, error) + OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) + OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) + OnGetTrustDomain func() (string, error) + OnFetchAttestationsWithSASURL func(attestations []*Attestation) ([]*Attestation, error) } func (m MockClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 9755e46f595..41da7101013 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -102,29 +102,19 @@ func GetRemoteAttestations(client api.Client, params FetchRemoteAttestationsPara // check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo. // If Repo is not set, the field will remain empty. It will not be populated using the value of Owner. if params.Repo != "" { - attestations, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit) + bundles, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit) if err != nil { - return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Repo, err) + return nil, fmt.Errorf("failed to fetch attestation bundles from %s: %w", params.Repo, err) } - fetched, err := client.FetchAttestationsWithSASURL(attestations) - if err != nil { - return nil, fmt.Errorf("failed to fetch bundles from SAS URL: %w", err) - } - - return fetched, nil + return bundles, nil } else if params.Owner != "" { - attestations, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit) + bundles, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Owner, err) } - fetched, err := client.FetchAttestationsWithSASURL(attestations) - if err != nil { - return nil, fmt.Errorf("failed to fetch bundles from SAS URL: %w", err) - } - - return fetched, nil + return bundles, nil } return nil, fmt.Errorf("owner or repo must be provided") } From e51b4efaa9008ebeaf373b91deed654987489c79 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 11:50:46 -0700 Subject: [PATCH 0040/2338] remove unused method Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 15 ++++++--------- pkg/cmd/attestation/api/mock_client.go | 11 +++-------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index acc4a101408..47cff27ed0a 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -32,7 +32,6 @@ type apiClient interface { } type Client interface { - // FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) GetTrustDomain() (string, error) @@ -65,35 +64,33 @@ func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Atte return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) } - compressedBundles, err := c.fetchBundlesWithSASURL(attestations) + bundles, err := c.fetchBundlesWithSASURL(attestations) if err != nil { return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) } - // decompress before returning - return compressedBundles, nil + return bundles, nil } -func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { +func (c *LiveClient) buildOwnerAndDigestURL(owner, digest string) string { owner = strings.Trim(owner, "/") return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest) } // GetByOwnerAndDigest fetches attestation by owner and digest func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { - url := c.BuildOwnerAndDigestURL(owner, digest) + url := c.buildOwnerAndDigestURL(owner, digest) attestations, err := c.getAttestations(url, owner, digest, limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) } - compressedBundles, err := c.fetchBundlesWithSASURL(attestations) + bundles, err := c.fetchBundlesWithSASURL(attestations) if err != nil { return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) } - // decompress before returning - return compressedBundles, nil + return bundles, nil } // GetTrustDomain returns the current trust domain. If the default is used diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go index 63dd3d24422..bcb51c414f5 100644 --- a/pkg/cmd/attestation/api/mock_client.go +++ b/pkg/cmd/attestation/api/mock_client.go @@ -7,14 +7,9 @@ import ( ) type MockClient struct { - OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) - OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) - OnGetTrustDomain func() (string, error) - OnFetchAttestationsWithSASURL func(attestations []*Attestation) ([]*Attestation, error) -} - -func (m MockClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { - return nil, nil + OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) + OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) + OnGetTrustDomain func() (string, error) } func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { From 6b95175363ccceb31259f88a216763a3dc02d345 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 11:57:42 -0700 Subject: [PATCH 0041/2338] add httpClient field to LiveClient struct Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 34 ++++++++++++++------------ pkg/cmd/attestation/api/client_test.go | 18 +++++++------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 47cff27ed0a..34be1a6bb0f 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -26,11 +26,15 @@ const ( // Allow injecting backoff interval in tests. var getAttestationRetryInterval = time.Millisecond * 200 -type apiClient interface { +type githubApiClient interface { REST(hostname, method, p string, body io.Reader, data interface{}) error RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) } +type httpClient interface { + Get(url string) (*http.Response, error) +} + type Client interface { GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) @@ -38,27 +42,29 @@ type Client interface { } type LiveClient struct { - api apiClient - host string - logger *ioconfig.Handler + githubAPI githubApiClient + httpClient httpClient + host string + logger *ioconfig.Handler } func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClient { return &LiveClient{ - api: api.NewClientFromHTTP(hc), - host: strings.TrimSuffix(host, "/"), - logger: l, + githubAPI: api.NewClientFromHTTP(hc), + host: strings.TrimSuffix(host, "/"), + httpClient: hc, + logger: l, } } -func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { +func (c *LiveClient) buildRepoAndDigestURL(repo, digest string) string { repo = strings.Trim(repo, "/") return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest) } // GetByRepoAndDigest fetches the attestation by repo and digest func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { - url := c.BuildRepoAndDigestURL(repo, digest) + url := c.buildRepoAndDigestURL(repo, digest) attestations, err := c.getAttestations(url, repo, digest, limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) @@ -121,7 +127,7 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At // if no attestation or less than limit, then keep fetching for url != "" && len(attestations) < limit { err := backoff.Retry(func() error { - newURL, restErr := c.api.RESTWithNext(c.host, http.MethodGet, url, nil, &resp) + newURL, restErr := c.githubAPI.RESTWithNext(c.host, http.MethodGet, url, nil, &resp) if restErr != nil { if shouldRetry(restErr) { @@ -170,14 +176,12 @@ func (c *LiveClient) fetchBundlesWithSASURL(attestations []*Attestation) ([]*Att func (c *LiveClient) fetchBundleWithSASURL(a *Attestation) (*bundle.Bundle, error) { if a.BundleURL == "" { - //return a.Bundle, nil - return nil, fmt.Errorf("SAS URL is empty") + return nil, fmt.Errorf("bundle URL is empty") } c.logger.VerbosePrintf("Fetching attestation bundle\n\n") - httpClient := http.DefaultClient - r, err := httpClient.Get(a.BundleURL) + r, err := c.httpClient.Get(a.BundleURL) if err != nil { return nil, err } @@ -225,7 +229,7 @@ func (c *LiveClient) getTrustDomain(url string) (string, error) { bo := backoff.NewConstantBackOff(getAttestationRetryInterval) err := backoff.Retry(func() error { - restErr := c.api.REST(c.host, http.MethodGet, url, nil, &resp) + restErr := c.githubAPI.REST(c.host, http.MethodGet, url, nil, &resp) if restErr != nil { if shouldRetry(restErr) { return restErr diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index adac0059898..fb3b338bf34 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -22,7 +22,7 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { if hasNextPage { return &LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, }, logger: l, @@ -30,7 +30,7 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { } return &LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccess, }, logger: l, @@ -50,7 +50,7 @@ func TestGetURL(t *testing.T) { } for _, data := range testData { - s := c.BuildRepoAndDigestURL(data.repo, data.digest) + s := c.buildRepoAndDigestURL(data.repo, data.digest) require.Equal(t, data.expected, s) } } @@ -135,7 +135,7 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { } c := LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, }, logger: io.NewTestHandler(), @@ -158,7 +158,7 @@ func TestGetByDigest_Error(t *testing.T) { } c := LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextError, }, logger: io.NewTestHandler(), @@ -180,7 +180,7 @@ func TestGetTrustDomain(t *testing.T) { t.Run("with returned trust domain", func(t *testing.T) { c := LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnREST: fetcher.OnREST, }, logger: io.NewTestHandler(), @@ -193,7 +193,7 @@ func TestGetTrustDomain(t *testing.T) { t.Run("with error", func(t *testing.T) { c := LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnREST: fetcher.OnRESTError, }, logger: io.NewTestHandler(), @@ -213,7 +213,7 @@ func TestGetAttestationsRetries(t *testing.T) { } c := &LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.FlakyOnRESTSuccessWithNextPageHandler(), }, logger: io.NewTestHandler(), @@ -252,7 +252,7 @@ func TestGetAttestationsMaxRetries(t *testing.T) { } c := &LiveClient{ - api: mockAPIClient{ + githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnREST500ErrorHandler(), }, logger: io.NewTestHandler(), From 45121f6674345a7b56ec8ef1153f7b2aee8c1a6f Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 12:02:42 -0700 Subject: [PATCH 0042/2338] go mod tidy Signed-off-by: Meredith Lancaster --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d2179290da9..5a31888757e 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/distribution/reference v0.5.0 github.com/gabriel-vasile/mimetype v1.4.7 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/shlex v0.0.0-20191202100458-e7afc7fbc510 @@ -92,7 +93,6 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/golang/snappy v0.0.4 // 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 From 8f5d7100f5056cc375a6c58dabc9dfaf55424769 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 12:02:52 -0700 Subject: [PATCH 0043/2338] var naming Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 34be1a6bb0f..7646b04e172 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -181,24 +181,24 @@ func (c *LiveClient) fetchBundleWithSASURL(a *Attestation) (*bundle.Bundle, erro c.logger.VerbosePrintf("Fetching attestation bundle\n\n") - r, err := c.httpClient.Get(a.BundleURL) + resp, err := c.httpClient.Get(a.BundleURL) if err != nil { return nil, err } - defer r.Body.Close() - if r.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", r.StatusCode) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } // parse the URL to get the host and path - resp, err := io.ReadAll(r.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read blob storage response body: %w", err) } var out []byte - decompressed, err := snappy.Decode(out, resp) + decompressed, err := snappy.Decode(out, body) if err != nil { return nil, fmt.Errorf("failed to decompress with snappy: %w", err) } From fb020f2a79aefa878f1e84d33410e18c8bb4e5f1 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 12:13:22 -0700 Subject: [PATCH 0044/2338] update error messages Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 7646b04e172..fc5e06f7bd9 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -70,9 +70,9 @@ func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Atte return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) } - bundles, err := c.fetchBundlesWithSASURL(attestations) + bundles, err := c.fetchBundlesByURL(attestations) if err != nil { - return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) } return bundles, nil @@ -91,9 +91,9 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) } - bundles, err := c.fetchBundlesWithSASURL(attestations) + bundles, err := c.fetchBundlesByURL(attestations) if err != nil { - return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) } return bundles, nil @@ -160,12 +160,12 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At return attestations, nil } -func (c *LiveClient) fetchBundlesWithSASURL(attestations []*Attestation) ([]*Attestation, error) { +func (c *LiveClient) fetchBundlesByURL(attestations []*Attestation) ([]*Attestation, error) { fetched := make([]*Attestation, len(attestations)) for i, a := range attestations { - b, err := c.fetchBundleWithSASURL(a) + b, err := c.fetchBundleByURL(a) if err != nil { - return nil, fmt.Errorf("failed to fetch bundle with SAS URL: %w", err) + return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) } fetched[i] = &Attestation{ Bundle: b, @@ -174,7 +174,7 @@ func (c *LiveClient) fetchBundlesWithSASURL(attestations []*Attestation) ([]*Att return fetched, nil } -func (c *LiveClient) fetchBundleWithSASURL(a *Attestation) (*bundle.Bundle, error) { +func (c *LiveClient) fetchBundleByURL(a *Attestation) (*bundle.Bundle, error) { if a.BundleURL == "" { return nil, fmt.Errorf("bundle URL is empty") } From e4431a3f55de6c9a20394e31aaa91f5f40a8d191 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 12:22:20 -0700 Subject: [PATCH 0045/2338] add mock http client Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 6 +++++ pkg/cmd/attestation/api/mock_client.go | 2 +- ...t_test.go => mock_githubApiClient_test.go} | 0 .../attestation/api/mock_httpClient_test.go | 25 +++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) rename pkg/cmd/attestation/api/{mock_apiClient_test.go => mock_githubApiClient_test.go} (100%) create mode 100644 pkg/cmd/attestation/api/mock_httpClient_test.go diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index fb3b338bf34..9e47870cf47 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -25,6 +25,9 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, }, + httpClient: mockHttpClient{ + OnGet: fetcher.OnGetSuccess, + }, logger: l, } } @@ -33,6 +36,9 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccess, }, + httpClient: mockHttpClient{ + OnGet: fetcher.OnGetSuccess, + }, logger: l, } } diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go index bcb51c414f5..b2fd334c056 100644 --- a/pkg/cmd/attestation/api/mock_client.go +++ b/pkg/cmd/attestation/api/mock_client.go @@ -25,7 +25,7 @@ func (m MockClient) GetTrustDomain() (string, error) { } func makeTestAttestation() Attestation { - return Attestation{Bundle: data.SigstoreBundle(nil)} + return Attestation{Bundle: data.SigstoreBundle(nil), BundleURL: "https://example.com"} } func OnGetByRepoAndDigestSuccess(repo, digest string, limit int) ([]*Attestation, error) { diff --git a/pkg/cmd/attestation/api/mock_apiClient_test.go b/pkg/cmd/attestation/api/mock_githubApiClient_test.go similarity index 100% rename from pkg/cmd/attestation/api/mock_apiClient_test.go rename to pkg/cmd/attestation/api/mock_githubApiClient_test.go diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go new file mode 100644 index 00000000000..70eceeea586 --- /dev/null +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -0,0 +1,25 @@ +package api + +import ( + "io" + "net/http" + "strings" + + "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" +) + +type mockHttpClient struct { + OnGet func(url string) (*http.Response, error) +} + +func (m mockHttpClient) Get(url string) (*http.Response, error) { + return m.OnGet(url) +} + +func (m *mockDataGenerator) OnGetSuccess(url string) (*http.Response, error) { + bundle := data.SigstoreBundle(nil) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(bundle.String())), + }, nil +} From ab4912ff48f8aea8ec48e96079e46ea4af8c546f Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 12:40:13 -0700 Subject: [PATCH 0046/2338] fix failing tests Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 6 +++++- pkg/cmd/attestation/api/client_test.go | 6 ++++++ pkg/cmd/attestation/api/mock_httpClient_test.go | 7 ++++--- pkg/cmd/attestation/verification/attestation.go | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index fc5e06f7bd9..2519aa9e48f 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -88,7 +88,11 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At url := c.buildOwnerAndDigestURL(owner, digest) attestations, err := c.getAttestations(url, owner, digest, limit) if err != nil { - return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) + return nil, err + } + + if len(attestations) == 0 { + return nil, newErrNoAttestations(owner, digest) } bundles, err := c.fetchBundlesByURL(attestations) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 9e47870cf47..540ae1366d9 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -144,6 +144,9 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, }, + httpClient: mockHttpClient{ + OnGet: fetcher.OnGetSuccess, + }, logger: io.NewTestHandler(), } @@ -222,6 +225,9 @@ func TestGetAttestationsRetries(t *testing.T) { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.FlakyOnRESTSuccessWithNextPageHandler(), }, + httpClient: mockHttpClient{ + OnGet: fetcher.OnGetSuccess, + }, logger: io.NewTestHandler(), } diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 70eceeea586..89584874fa1 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -1,11 +1,12 @@ package api import ( + "bytes" "io" "net/http" - "strings" "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" + "github.com/golang/snappy" ) type mockHttpClient struct { @@ -17,9 +18,9 @@ func (m mockHttpClient) Get(url string) (*http.Response, error) { } func (m *mockDataGenerator) OnGetSuccess(url string) (*http.Response, error) { - bundle := data.SigstoreBundle(nil) + compressed := snappy.Encode(nil, data.SigstoreBundleRaw) return &http.Response{ StatusCode: 200, - Body: io.NopCloser(strings.NewReader(bundle.String())), + Body: io.NopCloser(bytes.NewReader(compressed)), }, nil } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 41da7101013..8022a49434a 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -104,7 +104,7 @@ func GetRemoteAttestations(client api.Client, params FetchRemoteAttestationsPara if params.Repo != "" { bundles, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit) if err != nil { - return nil, fmt.Errorf("failed to fetch attestation bundles from %s: %w", params.Repo, err) + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Repo, err) } return bundles, nil From f3982b278cb1fc42f3a0664c03ac318df1a407ef Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 16 Dec 2024 16:57:05 -0700 Subject: [PATCH 0047/2338] add more verification integration tests Signed-off-by: Meredith Lancaster --- .../verify/verify-with-custom-trusted-root.sh | 22 +++++++++++++++++++ .../verify-with-internal-github-sigstore.sh | 16 ++++++++++++++ 2 files changed, 38 insertions(+) create mode 100755 test/integration/attestation-cmd/verify/verify-with-custom-trusted-root.sh create mode 100644 test/integration/attestation-cmd/verify/verify-with-internal-github-sigstore.sh diff --git a/test/integration/attestation-cmd/verify/verify-with-custom-trusted-root.sh b/test/integration/attestation-cmd/verify/verify-with-custom-trusted-root.sh new file mode 100755 index 00000000000..89a3a4556b2 --- /dev/null +++ b/test/integration/attestation-cmd/verify/verify-with-custom-trusted-root.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Get the root directory of the repository +rootDir="$(git rev-parse --show-toplevel)" + +ghBuildPath="$rootDir/bin/gh" + +artifactPath="$rootDir/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz" +bundlePath="$rootDir/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + +# Download a custom trusted root for verification +if ! $ghBuildPath attestation trusted-root > trusted_root.jsonl; then + # cleanup test data + echo "Failed to download trusted root" + exit 1 +fi + +if ! $ghBuildPath attestation verify "$artifactPath" -b "$bundlePath" --digest-alg=sha512 --owner=sigstore --custom-trusted-root trusted_root.jsonl; then + echo "Failed to verify package with a Sigstore v0.2.0 bundle" + exit 1 +fi diff --git a/test/integration/attestation-cmd/verify/verify-with-internal-github-sigstore.sh b/test/integration/attestation-cmd/verify/verify-with-internal-github-sigstore.sh new file mode 100644 index 00000000000..647a13a4c9c --- /dev/null +++ b/test/integration/attestation-cmd/verify/verify-with-internal-github-sigstore.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Get the root directory of the repository +rootDir="$(git rev-parse --show-toplevel)" + +ghBuildPath="$rootDir/bin/gh" + +ghCLIArtifact="$rootDir/pkg/cmd/attestation/test/data/gh_2.60.1_windows_arm64.zip" + +# Verify the gh CLI artifact +echo "Testing with package $ghCLIArtifact" +if ! $ghBuildPath attestation verify "$ghCLIArtifact" --digest-alg=sha256 --owner=cli; then + echo "Failed to verify" + exit 1 +fi From ce6150d136a048e4d235e313ebeeaf451f5dcb60 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 19 Dec 2024 07:18:01 -0700 Subject: [PATCH 0048/2338] simplify func params Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy.go | 12 ++++++------ pkg/cmd/attestation/verify/policy_test.go | 9 +-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 41b9ea27eb8..1e1dcd4d892 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -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/]///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 } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index 30724afefad..d033ba4fabd 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -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 { From 73cd38ac07fd7195b9d8778995b86dce296848cb Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 19 Dec 2024 12:47:55 -0700 Subject: [PATCH 0049/2338] update verify output table Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 016ec1fa8d1..3171b12787d 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -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,7 +262,7 @@ func runVerify(opts *Options) error { return nil } - opts.Logger.Printf("%s was attested by:\n", artifact.DigestWithAlg()) + opts.Logger.Printf("%s was attested by %s with the following attributes:\n\n", artifact.DigestWithAlg(), text.Pluralize(len(verified), "attestation")) // Otherwise print the results to the terminal in a table tableContent, err := buildTableVerifyContent(opts.Tenant, verified) @@ -270,7 +271,7 @@ func runVerify(opts *Options) error { return err } - headers := []string{"repo", "predicate_type", "workflow"} + headers := []string{"signer repo", "signer workflow", "build repo", "build 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 @@ -304,13 +305,13 @@ 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] @@ -327,15 +328,19 @@ func buildTableVerifyContent(tenant string, results []*verification.AttestationP return nil, fmt.Errorf("bundle missing verification result fields") } builderSignerURI := res.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI - repoAndOrg, workflow, err := extractAttestationDetail(tenant, builderSignerURI) + buildRepoAndOrg, buildWorkflow, 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)") + + //sourceRepoURI := res.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI + buildConfigURI := res.VerificationResult.Signature.Certificate.Extensions.BuildConfigURI + sourceRepoAndOrg, sourceWorkflow, err := extractAttestationDetail(tenant, buildConfigURI) + if err != nil { + return nil, err } - predicateType := res.VerificationResult.Statement.PredicateType - content[i] = []string{repoAndOrg, predicateType, workflow} + + content[i] = []string{buildRepoAndOrg, buildWorkflow, sourceRepoAndOrg, sourceWorkflow} } return content, nil From 1386e3671fd630cc72aab49ca5c532d146ed6583 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 20 Dec 2024 14:55:29 -0800 Subject: [PATCH 0050/2338] Add job to deployment workflow to validate the tag name for a given release --- .github/workflows/deployment.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 2dd6af675be..6c166580e27 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -28,7 +28,17 @@ on: default: true jobs: + validate-tag-name: + runs-on: ubuntu-latest + steps: + - name: Validate tag name format + run: | + if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+.[0-9]+.[0-9]+$]]; then + echo "Invalid tag name format. Must be in the form v1.2.3" + exit 1 + fi linux: + needs: validate-tag-name runs-on: ubuntu-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'linux') @@ -63,6 +73,7 @@ jobs: dist/*.deb macos: + needs: validate-tag-name runs-on: macos-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'macos') @@ -134,6 +145,7 @@ jobs: dist/*.pkg windows: + needs: validate-tag-name runs-on: windows-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'windows') From a542011cdce8fb579089a0e6a8284526a19d21d3 Mon Sep 17 00:00:00 2001 From: ChandranshuRao14 Date: Tue, 24 Dec 2024 12:59:42 -0600 Subject: [PATCH 0051/2338] Feat: Allow setting security_and_analysis settings in gh repo edit --- pkg/cmd/repo/edit/edit.go | 92 ++++++++++++++++++++++++++++------ pkg/cmd/repo/edit/edit_test.go | 30 +++++++++++ 2 files changed, 106 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 29a149cf975..80e460186ae 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -66,22 +66,37 @@ type EditOptions struct { } type EditRepositoryInput struct { - AllowForking *bool `json:"allow_forking,omitempty"` - AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"` - DefaultBranch *string `json:"default_branch,omitempty"` - DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` - Description *string `json:"description,omitempty"` - EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` - EnableIssues *bool `json:"has_issues,omitempty"` - EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` - EnableProjects *bool `json:"has_projects,omitempty"` - EnableDiscussions *bool `json:"has_discussions,omitempty"` - EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` - EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` - EnableWiki *bool `json:"has_wiki,omitempty"` - Homepage *string `json:"homepage,omitempty"` - IsTemplate *bool `json:"is_template,omitempty"` - Visibility *string `json:"visibility,omitempty"` + enableAdvancedSecurity *bool + enableSecretScanning *bool + enableSecretScanningPushProtection *bool + + AllowForking *bool `json:"allow_forking,omitempty"` + AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"` + DefaultBranch *string `json:"default_branch,omitempty"` + DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` + Description *string `json:"description,omitempty"` + EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` + EnableIssues *bool `json:"has_issues,omitempty"` + EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` + EnableProjects *bool `json:"has_projects,omitempty"` + EnableDiscussions *bool `json:"has_discussions,omitempty"` + EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` + EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` + EnableWiki *bool `json:"has_wiki,omitempty"` + Homepage *string `json:"homepage,omitempty"` + IsTemplate *bool `json:"is_template,omitempty"` + SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"` + Visibility *string `json:"visibility,omitempty"` +} + +type SecurityAndAnalysisInput struct { + EnableAdvancedSecurity *SecurityAndAnalysisStatus `json:"advanced_security,omitempty"` + EnableSecretScanning *SecurityAndAnalysisStatus `json:"secret_scanning,omitempty"` + EnableSecretScanningPushProtection *SecurityAndAnalysisStatus `json:"secret_scanning_push_protection,omitempty"` +} + +type SecurityAndAnalysisStatus struct { + Status *string `json:"status,omitempty"` } func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command { @@ -177,6 +192,9 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableSquashMerge, "enable-squash-merge", "", "Enable merging pull requests via squashed commit") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableRebaseMerge, "enable-rebase-merge", "", "Enable merging pull requests via rebase") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableAutoMerge, "enable-auto-merge", "", "Enable auto-merge functionality") + cmdutil.NilBoolFlag(cmd, &opts.Edits.enableAdvancedSecurity, "enable-advanced-security", "", "Enable advanced security in the repository") + cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanning, "enable-secret-scanning", "", "Enable secret scanning in the repository") + cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanningPushProtection, "enable-secret-scanning-push-protection", "", "Enable secret scanning push protection in the repository. Secret scanning must be enabled first") cmdutil.NilBoolFlag(cmd, &opts.Edits.DeleteBranchOnMerge, "delete-branch-on-merge", "", "Delete head branch when pull requests are merged") cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowForking, "allow-forking", "", "Allow forking of an organization repository") cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowUpdateBranch, "allow-update-branch", "", "Allow a pull request head branch that is behind its base branch to be updated") @@ -240,6 +258,34 @@ func editRun(ctx context.Context, opts *EditOptions) error { } } + if hasSecurityEdits(opts.Edits) { + apiClient := api.NewClientFromHTTP(opts.HTTPClient) + repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"}) + if err != nil { + return err + } + if !repo.ViewerCanAdminister { + return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features") + } + + opts.Edits.SecurityAndAnalysis = &SecurityAndAnalysisInput{} + if opts.Edits.enableAdvancedSecurity != nil { + opts.Edits.SecurityAndAnalysis.EnableAdvancedSecurity = &SecurityAndAnalysisStatus{ + Status: boolToStatus(*opts.Edits.enableAdvancedSecurity), + } + } + if opts.Edits.enableSecretScanning != nil { + opts.Edits.SecurityAndAnalysis.EnableSecretScanning = &SecurityAndAnalysisStatus{ + Status: boolToStatus(*opts.Edits.enableSecretScanning), + } + } + if opts.Edits.enableSecretScanningPushProtection != nil { + opts.Edits.SecurityAndAnalysis.EnableSecretScanningPushProtection = &SecurityAndAnalysisStatus{ + Status: boolToStatus(*opts.Edits.enableSecretScanningPushProtection), + } + } + } + apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName()) body := &bytes.Buffer{} @@ -560,3 +606,17 @@ func isIncluded(value string, opts []string) bool { } return false } + +func boolToStatus(status bool) *string { + var result string + if status { + result = "enabled" + } else { + result = "disabled" + } + return &result +} + +func hasSecurityEdits(edits EditRepositoryInput) bool { + return edits.enableAdvancedSecurity != nil || edits.enableSecretScanning != nil || edits.enableSecretScanningPushProtection != nil +} diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index 217c1dce46c..93d44788d31 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -201,6 +201,36 @@ func Test_editRun(t *testing.T) { })) }, }, + { + name: "enable/disable security and analysis settings", + opts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SecurityAndAnalysis: &SecurityAndAnalysisInput{ + EnableAdvancedSecurity: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanning: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{ + Status: sp("disabled"), + }, + }, + }, + }, + httpStubs: func(t *testing.T, r *httpmock.Registry) { + r.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO"), + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { + assert.Equal(t, 1, len(payload)) + securityAndAnalysis := payload["security_and_analysis"].(map[string]interface{}) + assert.Equal(t, "enabled", securityAndAnalysis["advanced_security"].(map[string]interface{})["status"]) + assert.Equal(t, "enabled", securityAndAnalysis["secret_scanning"].(map[string]interface{})["status"]) + assert.Equal(t, "disabled", securityAndAnalysis["secret_scanning_push_protection"].(map[string]interface{})["status"]) + })) + }, + }, } for _, tt := range tests { From 5fc56ded50e737950b7032c72e42ed6de5ba2083 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 26 Dec 2024 10:50:09 -0800 Subject: [PATCH 0052/2338] Escape '.' in version validation regex in deployment workflow --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 6c166580e27..88492ef2cf3 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Validate tag name format run: | - if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+.[0-9]+.[0-9]+$]]; then + if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$]]; then echo "Invalid tag name format. Must be in the form v1.2.3" exit 1 fi From 29371e949126db53cc8d93132d23812a06fc72fd Mon Sep 17 00:00:00 2001 From: ChandranshuRao14 Date: Thu, 26 Dec 2024 22:52:32 -0500 Subject: [PATCH 0053/2338] Extract logic into helper function --- pkg/cmd/repo/edit/edit.go | 82 ++++++++++++++++--------------- pkg/cmd/repo/edit/edit_test.go | 89 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 38 deletions(-) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 80e460186ae..8efcea11daa 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -89,16 +89,6 @@ type EditRepositoryInput struct { Visibility *string `json:"visibility,omitempty"` } -type SecurityAndAnalysisInput struct { - EnableAdvancedSecurity *SecurityAndAnalysisStatus `json:"advanced_security,omitempty"` - EnableSecretScanning *SecurityAndAnalysisStatus `json:"secret_scanning,omitempty"` - EnableSecretScanningPushProtection *SecurityAndAnalysisStatus `json:"secret_scanning_push_protection,omitempty"` -} - -type SecurityAndAnalysisStatus struct { - Status *string `json:"status,omitempty"` -} - func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command { opts := &EditOptions{ IO: f.IOStreams, @@ -172,6 +162,18 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr return cmdutil.FlagErrorf("use of --visibility flag requires --accept-visibility-change-consequences flag") } + if hasSecurityEdits(opts.Edits) { + apiClient := api.NewClientFromHTTP(opts.HTTPClient) + repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"}) + if err != nil { + return err + } + if !repo.ViewerCanAdminister { + return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features") + } + opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts) + } + if runF != nil { return runF(opts) } @@ -258,34 +260,6 @@ func editRun(ctx context.Context, opts *EditOptions) error { } } - if hasSecurityEdits(opts.Edits) { - apiClient := api.NewClientFromHTTP(opts.HTTPClient) - repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"}) - if err != nil { - return err - } - if !repo.ViewerCanAdminister { - return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features") - } - - opts.Edits.SecurityAndAnalysis = &SecurityAndAnalysisInput{} - if opts.Edits.enableAdvancedSecurity != nil { - opts.Edits.SecurityAndAnalysis.EnableAdvancedSecurity = &SecurityAndAnalysisStatus{ - Status: boolToStatus(*opts.Edits.enableAdvancedSecurity), - } - } - if opts.Edits.enableSecretScanning != nil { - opts.Edits.SecurityAndAnalysis.EnableSecretScanning = &SecurityAndAnalysisStatus{ - Status: boolToStatus(*opts.Edits.enableSecretScanning), - } - } - if opts.Edits.enableSecretScanningPushProtection != nil { - opts.Edits.SecurityAndAnalysis.EnableSecretScanningPushProtection = &SecurityAndAnalysisStatus{ - Status: boolToStatus(*opts.Edits.enableSecretScanningPushProtection), - } - } - } - apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName()) body := &bytes.Buffer{} @@ -620,3 +594,35 @@ func boolToStatus(status bool) *string { func hasSecurityEdits(edits EditRepositoryInput) bool { return edits.enableAdvancedSecurity != nil || edits.enableSecretScanning != nil || edits.enableSecretScanningPushProtection != nil } + +type SecurityAndAnalysisInput struct { + EnableAdvancedSecurity *SecurityAndAnalysisStatus `json:"advanced_security,omitempty"` + EnableSecretScanning *SecurityAndAnalysisStatus `json:"secret_scanning,omitempty"` + EnableSecretScanningPushProtection *SecurityAndAnalysisStatus `json:"secret_scanning_push_protection,omitempty"` +} + +type SecurityAndAnalysisStatus struct { + Status *string `json:"status,omitempty"` +} + +// Transform security and analysis parameters to properly serialize EditRepositoryInput +// See API Docs: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#update-a-repository +func transformSecurityAndAnalysisOpts(opts *EditOptions) *SecurityAndAnalysisInput { + securityOptions := &SecurityAndAnalysisInput{} + if opts.Edits.enableAdvancedSecurity != nil { + securityOptions.EnableAdvancedSecurity = &SecurityAndAnalysisStatus{ + Status: boolToStatus(*opts.Edits.enableAdvancedSecurity), + } + } + if opts.Edits.enableSecretScanning != nil { + securityOptions.EnableSecretScanning = &SecurityAndAnalysisStatus{ + Status: boolToStatus(*opts.Edits.enableSecretScanning), + } + } + if opts.Edits.enableSecretScanningPushProtection != nil { + securityOptions.EnableSecretScanningPushProtection = &SecurityAndAnalysisStatus{ + Status: boolToStatus(*opts.Edits.enableSecretScanningPushProtection), + } + } + return securityOptions +} diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index 93d44788d31..93b2564654a 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -700,6 +700,95 @@ func Test_editRun_interactive(t *testing.T) { } } +func Test_transformSecurityAndAnalysisOpts(t *testing.T) { + tests := []struct { + name string + opts EditOptions + want *SecurityAndAnalysisInput + }{ + { + name: "Enable all security and analysis settings", + opts: EditOptions{ + Edits: EditRepositoryInput{ + enableAdvancedSecurity: bp(true), + enableSecretScanning: bp(true), + enableSecretScanningPushProtection: bp(true), + }, + }, + want: &SecurityAndAnalysisInput{ + EnableAdvancedSecurity: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanning: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + }, + }, + { + name: "Disable all security and analysis settings", + opts: EditOptions{ + Edits: EditRepositoryInput{ + enableAdvancedSecurity: bp(false), + enableSecretScanning: bp(false), + enableSecretScanningPushProtection: bp(false), + }, + }, + want: &SecurityAndAnalysisInput{ + EnableAdvancedSecurity: &SecurityAndAnalysisStatus{ + Status: sp("disabled"), + }, + EnableSecretScanning: &SecurityAndAnalysisStatus{ + Status: sp("disabled"), + }, + EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{ + Status: sp("disabled"), + }, + }, + }, + { + name: "Enable only advanced security", + opts: EditOptions{ + Edits: EditRepositoryInput{ + enableAdvancedSecurity: bp(true), + }, + }, + want: &SecurityAndAnalysisInput{ + EnableAdvancedSecurity: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanning: nil, + EnableSecretScanningPushProtection: nil, + }, + }, + { + name: "Disable only secret scanning", + opts: EditOptions{ + Edits: EditRepositoryInput{ + enableSecretScanning: bp(false), + }, + }, + want: &SecurityAndAnalysisInput{ + EnableAdvancedSecurity: nil, + EnableSecretScanning: &SecurityAndAnalysisStatus{ + Status: sp("disabled"), + }, + EnableSecretScanningPushProtection: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := &tt.opts + transformed := transformSecurityAndAnalysisOpts(opts) + assert.Equal(t, tt.want, transformed) + }) + } +} + func sp(v string) *string { return &v } From 23c16c9c4cb033840065e0c1d08dbdf6cce7ca3f Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 20 Dec 2024 13:06:47 -0500 Subject: [PATCH 0054/2338] Introduce repo autolinks list commands --- pkg/cmd/repo/autolink/autolink.go | 19 +++ pkg/cmd/repo/autolink/http.go | 44 +++++ pkg/cmd/repo/autolink/list.go | 123 ++++++++++++++ pkg/cmd/repo/autolink/list_test.go | 256 +++++++++++++++++++++++++++++ pkg/cmd/repo/autolink/shared.go | 14 ++ pkg/cmd/repo/repo.go | 3 + 6 files changed, 459 insertions(+) create mode 100644 pkg/cmd/repo/autolink/autolink.go create mode 100644 pkg/cmd/repo/autolink/http.go create mode 100644 pkg/cmd/repo/autolink/list.go create mode 100644 pkg/cmd/repo/autolink/list_test.go create mode 100644 pkg/cmd/repo/autolink/shared.go diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go new file mode 100644 index 00000000000..1c3c16d447d --- /dev/null +++ b/pkg/cmd/repo/autolink/autolink.go @@ -0,0 +1,19 @@ +package autolink + +import ( + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" +) + +func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "autolink ", + Short: "Manage autolink references", + Long: "Work with GitHub autolink references.", + } + cmdutil.EnableRepoOverride(cmd, f) + + cmd.AddCommand(newCmdList(f, nil)) + + return cmd +} diff --git a/pkg/cmd/repo/autolink/http.go b/pkg/cmd/repo/autolink/http.go new file mode 100644 index 00000000000..b8316a336db --- /dev/null +++ b/pkg/cmd/repo/autolink/http.go @@ -0,0 +1,44 @@ +package autolink + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" +) + +func repoAutolinks(httpClient *http.Client, repo ghrepo.Interface) ([]autolink, error) { + path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) + url := ghinstance.RESTPrefix(repo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return nil, api.HandleHTTPError(resp) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var autolinks []autolink + err = json.Unmarshal(b, &autolinks) + if err != nil { + return nil, err + } + + return autolinks, nil +} diff --git a/pkg/cmd/repo/autolink/list.go b/pkg/cmd/repo/autolink/list.go new file mode 100644 index 00000000000..45606bfa928 --- /dev/null +++ b/pkg/cmd/repo/autolink/list.go @@ -0,0 +1,123 @@ +package autolink + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/tableprinter" + "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +var autolinkFields = []string{ + "id", + "isAlphanumeric", + "keyPrefix", + "urlTemplate", +} + +type listOptions struct { + BaseRepo func() (ghrepo.Interface, error) + Browser browser.Browser + HTTPClient func() (*http.Client, error) + IO *iostreams.IOStreams + + Exporter cmdutil.Exporter + WebMode bool +} + +func newCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { + opts := &listOptions{ + Browser: f.Browser, + HTTPClient: f.HttpClient, + IO: f.IOStreams, + } + + cmd := &cobra.Command{ + Use: "list", + Short: "List autolink references for a GitHub repository", + Long: heredoc.Doc(` + Gets all autolink references that are configured for a repository. + + Information about autolinks is only available to repository administrators. + `), + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + opts.BaseRepo = f.BaseRepo + + if runF != nil { + return runF(opts) + } + + return listRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List autolink references in the web browser") + cmdutil.AddJSONFlags(cmd, &opts.Exporter, autolinkFields) + + return cmd +} + +func listRun(opts *listOptions) error { + client, err := opts.HTTPClient() + if err != nil { + return err + } + + repo, err := opts.BaseRepo() + if err != nil { + return err + } + + if opts.WebMode { + autolinksListURL := ghrepo.GenerateRepoURL(repo, "settings/key_links") + + if opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(autolinksListURL)) + } + + return opts.Browser.Browse(autolinksListURL) + } + + autolinks, err := repoAutolinks(client, repo) + if err != nil { + return err + } + + if len(autolinks) == 0 { + return cmdutil.NewNoResultsError(fmt.Sprintf("no autolinks found in %s", ghrepo.FullName(repo))) + } + + if opts.Exporter != nil { + return opts.Exporter.Write(opts.IO, autolinks) + } + + if opts.IO.IsStdoutTTY() { + title := listHeader(ghrepo.FullName(repo), len(autolinks)) + fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title) + } + + tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC")) + + for _, autolink := range autolinks { + tp.AddField(fmt.Sprintf("%d", autolink.ID)) + tp.AddField(autolink.KeyPrefix) + tp.AddField(autolink.URLTemplate) + tp.AddField(strconv.FormatBool(autolink.IsAlphanumeric)) + tp.EndRow() + } + + return tp.Render() +} + +func listHeader(repoName string, count int) string { + return fmt.Sprintf("Showing %s in %s", text.Pluralize(count, "autolink reference"), repoName) +} diff --git a/pkg/cmd/repo/autolink/list_test.go b/pkg/cmd/repo/autolink/list_test.go new file mode 100644 index 00000000000..2e3c2137427 --- /dev/null +++ b/pkg/cmd/repo/autolink/list_test.go @@ -0,0 +1,256 @@ +package autolink + +import ( + "bytes" + "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/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdList(t *testing.T) { + tests := []struct { + name string + input string + output listOptions + wantErr bool + wantExporter bool + errMsg string + }{ + { + name: "no argument", + input: "", + output: listOptions{}, + }, + { + name: "web flag", + input: "--web", + output: listOptions{WebMode: true}, + }, + { + name: "json flag", + input: "--json id", + output: listOptions{}, + wantExporter: true, + }, + { + name: "invalid json flag", + input: "--json invalid", + wantErr: true, + errMsg: heredoc.Doc(` + Unknown JSON field: "invalid" + Available fields: + id + isAlphanumeric + keyPrefix + urlTemplate`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: ios, + } + + argv, err := shlex.Split(tt.input) + require.NoError(t, err) + + var gotOpts *listOptions + cmd := newCmdList(f, func(opts *listOptions) 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) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) + assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil) + }) + } +} + +func TestListRun(t *testing.T) { + tests := []struct { + name string + opts *listOptions + isTTY bool + httpStubs func(t *testing.T, reg *httpmock.Registry) + wantStdout string + wantStderr string + wantErr bool + }{ + { + name: "list tty", + opts: &listOptions{}, + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), + httpmock.StringResponse(`[ + { + "id": 1, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": true + }, + { + "id": 2, + "key_prefix": "STORY-", + "url_template": "https://example.com/STORY?id=", + "is_alphanumeric": false + } + ]`), + ) + }, + wantStdout: heredoc.Doc(` + + Showing 2 autolink references in OWNER/REPO + + ID KEY PREFIX URL TEMPLATE ALPHANUMERIC + 1 TICKET- https://example.com/TICKET?query= true + 2 STORY- https://example.com/STORY?id= false + `), + wantStderr: "", + }, + { + name: "list json", + opts: &listOptions{ + Exporter: func() cmdutil.Exporter { + exporter := cmdutil.NewJSONExporter() + exporter.SetFields([]string{"id"}) + return exporter + }(), + }, + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), + httpmock.StringResponse(`[ + { + "id": 1, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": true + }, + { + "id": 2, + "key_prefix": "STORY-", + "url_template": "https://example.com/STORY?id=", + "is_alphanumeric": false + } + ]`), + ) + }, + wantStdout: "[{\"id\":1},{\"id\":2}]\n", + wantStderr: "", + }, + { + name: "list non-tty", + opts: &listOptions{}, + isTTY: false, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), + httpmock.StringResponse(`[ + { + "id": 1, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": true + }, + { + "id": 2, + "key_prefix": "STORY-", + "url_template": "https://example.com/STORY?id=", + "is_alphanumeric": false + } + ]`), + ) + }, + wantStdout: heredoc.Doc(` + 1 TICKET- https://example.com/TICKET?query= true + 2 STORY- https://example.com/STORY?id= false + `), + wantStderr: "", + }, + + { + name: "no results", + opts: &listOptions{}, + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), + httpmock.StringResponse(`[]`), + ) + }, + wantStdout: "", + wantStderr: "", + wantErr: true, + }, + { + name: "web mode", + isTTY: true, + opts: &listOptions{WebMode: true}, + wantStderr: "Opening https://github.com/OWNER/REPO/settings/key_links in your browser.\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(tt.isTTY) + ios.SetStdinTTY(tt.isTTY) + ios.SetStderrTTY(tt.isTTY) + + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(t, reg) + } + + defer reg.Verify(t) + + 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.HTTPClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } + + err := listRun(opts) + if (err != nil) != tt.wantErr { + t.Errorf("listRun() return error: %v", err) + return + } + + if stdout.String() != tt.wantStdout { + t.Errorf("wants stdout %q, got %q", tt.wantStdout, stdout.String()) + } + if stderr.String() != tt.wantStderr { + t.Errorf("wants stderr %q, got %q", tt.wantStderr, stderr.String()) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/shared.go b/pkg/cmd/repo/autolink/shared.go new file mode 100644 index 00000000000..c8478b346fb --- /dev/null +++ b/pkg/cmd/repo/autolink/shared.go @@ -0,0 +1,14 @@ +package autolink + +import "github.com/cli/cli/v2/pkg/cmdutil" + +type autolink struct { + ID int `json:"id"` + IsAlphanumeric bool `json:"is_alphanumeric"` + KeyPrefix string `json:"key_prefix"` + URLTemplate string `json:"url_template"` +} + +func (s *autolink) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(s, fields) +} diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index 14a4bf49cf5..687cf5a8aac 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -3,6 +3,7 @@ package repo import ( "github.com/MakeNowJust/heredoc" repoArchiveCmd "github.com/cli/cli/v2/pkg/cmd/repo/archive" + repoAutolinkCmd "github.com/cli/cli/v2/pkg/cmd/repo/autolink" repoCloneCmd "github.com/cli/cli/v2/pkg/cmd/repo/clone" repoCreateCmd "github.com/cli/cli/v2/pkg/cmd/repo/create" creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits" @@ -19,6 +20,7 @@ import ( repoSyncCmd "github.com/cli/cli/v2/pkg/cmd/repo/sync" repoUnarchiveCmd "github.com/cli/cli/v2/pkg/cmd/repo/unarchive" repoViewCmd "github.com/cli/cli/v2/pkg/cmd/repo/view" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -64,6 +66,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { repoDeleteCmd.NewCmdDelete(f, nil), creditsCmd.NewCmdRepoCredits(f, nil), gardenCmd.NewCmdGarden(f, nil), + repoAutolinkCmd.NewCmdAutolink(f), ) return cmd From 5fb98524e0d864dd36e6c6dd6fe25e5568f7fe3a Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Mon, 23 Dec 2024 14:34:21 -0800 Subject: [PATCH 0055/2338] Apply PR comment changes --- pkg/cmd/repo/autolink/autolink.go | 11 ++++++++++- pkg/cmd/repo/autolink/http.go | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index 1c3c16d447d..9c0972f7c3a 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -1,6 +1,7 @@ package autolink import ( + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -9,7 +10,15 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "autolink ", Short: "Manage autolink references", - Long: "Work with GitHub autolink references.", + Long: heredoc.Docf(` + Work with GitHub autolink references. + + GitHub autolinks require admin access to configure and can be found at + https://github.com/{owner}/{repo}/settings/key_links. + Use %[1]sgh repo autolink list --web%[1]s to open this page for the current repository. + + For more information about GitHub autolinks, see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources + `, "`"), } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/repo/autolink/http.go b/pkg/cmd/repo/autolink/http.go index b8316a336db..d51ac369c92 100644 --- a/pkg/cmd/repo/autolink/http.go +++ b/pkg/cmd/repo/autolink/http.go @@ -25,7 +25,9 @@ func repoAutolinks(httpClient *http.Client, repo ghrepo.Interface) ([]autolink, } defer resp.Body.Close() - if resp.StatusCode > 299 { + if resp.StatusCode == 404 { + return nil, fmt.Errorf("error getting autolinks: HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/%s)", path) + } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } From a390ce4f10677c8fd9d87ac49f08e1f05f29a709 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Mon, 23 Dec 2024 15:08:58 -0800 Subject: [PATCH 0056/2338] Refactor autolink list and test to use http interface for simpler testing This defines an AutolinkClient interface with a Get method used for fetching the autolinks lists from the api. Then, the http client for autolinks implements this interface with the AutolinkGetter struct. This allows for dependency injection of the AutolinkGetter struct into the listOptions, enabling mocking of the AutolinkGetter for testing. The result of this is simpler tests that are easier to maintain, because the interface for the table tests now allow for defining autolink structs as the response instead of large mocked api calls. This also allows for bespoke testing of the http file, which I'll follow up with in the next commit. --- pkg/cmd/repo/autolink/http.go | 14 ++- pkg/cmd/repo/autolink/list.go | 30 +++--- pkg/cmd/repo/autolink/list_test.go | 158 ++++++++++++++--------------- 3 files changed, 104 insertions(+), 98 deletions(-) diff --git a/pkg/cmd/repo/autolink/http.go b/pkg/cmd/repo/autolink/http.go index d51ac369c92..13e8adebeaf 100644 --- a/pkg/cmd/repo/autolink/http.go +++ b/pkg/cmd/repo/autolink/http.go @@ -11,7 +11,17 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" ) -func repoAutolinks(httpClient *http.Client, repo ghrepo.Interface) ([]autolink, error) { +type AutolinkGetter struct { + HttpClient *http.Client +} + +func NewAutolinkGetter(httpClient *http.Client) *AutolinkGetter { + return &AutolinkGetter{ + HttpClient: httpClient, + } +} + +func (a *AutolinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path req, err := http.NewRequest("GET", url, nil) @@ -19,7 +29,7 @@ func repoAutolinks(httpClient *http.Client, repo ghrepo.Interface) ([]autolink, return nil, err } - resp, err := httpClient.Do(req) + resp, err := a.HttpClient.Do(req) if err != nil { return nil, err } diff --git a/pkg/cmd/repo/autolink/list.go b/pkg/cmd/repo/autolink/list.go index 45606bfa928..41d0d726639 100644 --- a/pkg/cmd/repo/autolink/list.go +++ b/pkg/cmd/repo/autolink/list.go @@ -2,7 +2,6 @@ package autolink import ( "fmt" - "net/http" "strconv" "github.com/MakeNowJust/heredoc" @@ -23,20 +22,23 @@ var autolinkFields = []string{ } type listOptions struct { - BaseRepo func() (ghrepo.Interface, error) - Browser browser.Browser - HTTPClient func() (*http.Client, error) - IO *iostreams.IOStreams + BaseRepo func() (ghrepo.Interface, error) + Browser browser.Browser + AutolinkClient AutolinkClient + IO *iostreams.IOStreams Exporter cmdutil.Exporter WebMode bool } +type AutolinkClient interface { + Get(repo ghrepo.Interface) ([]autolink, error) +} + func newCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { opts := &listOptions{ - Browser: f.Browser, - HTTPClient: f.HttpClient, - IO: f.IOStreams, + Browser: f.Browser, + IO: f.IOStreams, } cmd := &cobra.Command{ @@ -52,6 +54,12 @@ func newCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman RunE: func(cmd *cobra.Command, args []string) error { opts.BaseRepo = f.BaseRepo + httpClient, err := f.HttpClient() + if err != nil { + return err + } + opts.AutolinkClient = NewAutolinkGetter(httpClient) + if runF != nil { return runF(opts) } @@ -67,10 +75,6 @@ func newCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman } func listRun(opts *listOptions) error { - client, err := opts.HTTPClient() - if err != nil { - return err - } repo, err := opts.BaseRepo() if err != nil { @@ -87,7 +91,7 @@ func listRun(opts *listOptions) error { return opts.Browser.Browse(autolinksListURL) } - autolinks, err := repoAutolinks(client, repo) + autolinks, err := opts.AutolinkClient.Get(repo) if err != nil { return err } diff --git a/pkg/cmd/repo/autolink/list_test.go b/pkg/cmd/repo/autolink/list_test.go index 2e3c2137427..4a41f9ad2c4 100644 --- a/pkg/cmd/repo/autolink/list_test.go +++ b/pkg/cmd/repo/autolink/list_test.go @@ -2,6 +2,7 @@ package autolink import ( "bytes" + "fmt" "net/http" "testing" @@ -9,7 +10,6 @@ import ( "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" - "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" @@ -61,6 +61,9 @@ func TestNewCmdList(t *testing.T) { 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) @@ -89,12 +92,28 @@ func TestNewCmdList(t *testing.T) { } } +type mockAutoLinkGetter struct { + Response []autolink +} + +func (m *mockAutoLinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { + return m.Response, nil +} + +type mockAutoLinkGetterError struct { + err error +} + +func (me *mockAutoLinkGetterError) Get(repo ghrepo.Interface) ([]autolink, error) { + return nil, me.err +} + func TestListRun(t *testing.T) { tests := []struct { name string opts *listOptions isTTY bool - httpStubs func(t *testing.T, reg *httpmock.Registry) + response []autolink wantStdout string wantStderr string wantErr bool @@ -103,24 +122,19 @@ func TestListRun(t *testing.T) { name: "list tty", opts: &listOptions{}, isTTY: true, - httpStubs: func(t *testing.T, reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), - httpmock.StringResponse(`[ - { - "id": 1, - "key_prefix": "TICKET-", - "url_template": "https://example.com/TICKET?query=", - "is_alphanumeric": true - }, - { - "id": 2, - "key_prefix": "STORY-", - "url_template": "https://example.com/STORY?id=", - "is_alphanumeric": false - } - ]`), - ) + response: []autolink{ + { + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + { + ID: 2, + KeyPrefix: "STORY-", + URLTemplate: "https://example.com/STORY?id=", + IsAlphanumeric: false, + }, }, wantStdout: heredoc.Doc(` @@ -142,24 +156,19 @@ func TestListRun(t *testing.T) { }(), }, isTTY: true, - httpStubs: func(t *testing.T, reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), - httpmock.StringResponse(`[ - { - "id": 1, - "key_prefix": "TICKET-", - "url_template": "https://example.com/TICKET?query=", - "is_alphanumeric": true - }, - { - "id": 2, - "key_prefix": "STORY-", - "url_template": "https://example.com/STORY?id=", - "is_alphanumeric": false - } - ]`), - ) + response: []autolink{ + { + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + { + ID: 2, + KeyPrefix: "STORY-", + URLTemplate: "https://example.com/STORY?id=", + IsAlphanumeric: false, + }, }, wantStdout: "[{\"id\":1},{\"id\":2}]\n", wantStderr: "", @@ -168,24 +177,19 @@ func TestListRun(t *testing.T) { name: "list non-tty", opts: &listOptions{}, isTTY: false, - httpStubs: func(t *testing.T, reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), - httpmock.StringResponse(`[ - { - "id": 1, - "key_prefix": "TICKET-", - "url_template": "https://example.com/TICKET?query=", - "is_alphanumeric": true - }, - { - "id": 2, - "key_prefix": "STORY-", - "url_template": "https://example.com/STORY?id=", - "is_alphanumeric": false - } - ]`), - ) + response: []autolink{ + { + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + { + ID: 2, + KeyPrefix: "STORY-", + URLTemplate: "https://example.com/STORY?id=", + IsAlphanumeric: false, + }, }, wantStdout: heredoc.Doc(` 1 TICKET- https://example.com/TICKET?query= true @@ -195,15 +199,10 @@ func TestListRun(t *testing.T) { }, { - name: "no results", - opts: &listOptions{}, - isTTY: true, - httpStubs: func(t *testing.T, reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/autolinks"), - httpmock.StringResponse(`[]`), - ) - }, + name: "no results", + opts: &listOptions{}, + isTTY: true, + response: []autolink{}, wantStdout: "", wantStderr: "", wantErr: true, @@ -223,13 +222,6 @@ func TestListRun(t *testing.T) { ios.SetStdinTTY(tt.isTTY) ios.SetStderrTTY(tt.isTTY) - reg := &httpmock.Registry{} - if tt.httpStubs != nil { - tt.httpStubs(t, reg) - } - - defer reg.Verify(t) - opts := tt.opts opts.IO = ios opts.Browser = &browser.Stub{} @@ -237,19 +229,19 @@ func TestListRun(t *testing.T) { opts.IO = ios opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } - opts.HTTPClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } - - err := listRun(opts) - if (err != nil) != tt.wantErr { - t.Errorf("listRun() return error: %v", err) - return + if tt.wantErr { + opts.AutolinkClient = &mockAutoLinkGetterError{err: fmt.Errorf("mock error")} + err := listRun(opts) + require.Error(t, err) + } else { + opts.AutolinkClient = &mockAutoLinkGetter{Response: tt.response} + err := listRun(opts) + require.NoError(t, err) + assert.Equal(t, tt.wantStdout, stdout.String()) } - if stdout.String() != tt.wantStdout { - t.Errorf("wants stdout %q, got %q", tt.wantStdout, stdout.String()) - } - if stderr.String() != tt.wantStderr { - t.Errorf("wants stderr %q, got %q", tt.wantStderr, stderr.String()) + if tt.wantStderr != "" { + assert.Equal(t, tt.wantStderr, stderr.String()) } }) } From 4a74cc8856f3c0178856c2b5ec7c6afd3df3bc86 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Mon, 23 Dec 2024 15:43:02 -0800 Subject: [PATCH 0057/2338] Add testing for AutoLinkGetter This adds the missing mocked http tests to the http_test.go file. These tests were previously bundled with the tests in list_test.go, creating a testing pattern that was difficult to understand and maintain. The refactor in the previous commit replaced these tests with the AutolinkClient interface, allowing for the httpmocks to be isolated to the AutolinkGetter that implements that interface. --- pkg/cmd/repo/autolink/http.go | 2 +- pkg/cmd/repo/autolink/http_test.go | 79 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/repo/autolink/http_test.go diff --git a/pkg/cmd/repo/autolink/http.go b/pkg/cmd/repo/autolink/http.go index 13e8adebeaf..a10e98f7bb6 100644 --- a/pkg/cmd/repo/autolink/http.go +++ b/pkg/cmd/repo/autolink/http.go @@ -36,7 +36,7 @@ func (a *AutolinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { defer resp.Body.Close() if resp.StatusCode == 404 { - return nil, fmt.Errorf("error getting autolinks: HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/%s)", path) + return nil, fmt.Errorf("error getting autolinks: HTTP 404: Must have admin rights to Repository. (https://api.github.com/%s)", path) } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } diff --git a/pkg/cmd/repo/autolink/http_test.go b/pkg/cmd/repo/autolink/http_test.go new file mode 100644 index 00000000000..64ffc1e404f --- /dev/null +++ b/pkg/cmd/repo/autolink/http_test.go @@ -0,0 +1,79 @@ +package autolink + +import ( + "fmt" + "net/http" + "testing" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewAutolinkGetter(t *testing.T) { + httpClient := &http.Client{} + autolinkGetter := NewAutolinkGetter(httpClient) + assert.NotNil(t, autolinkGetter) +} + +func TestAutoLinkGetter_Get(t *testing.T) { + tests := []struct { + name string + repo ghrepo.Interface + resp []autolink + status int + }{ + { + name: "no autolinks", + repo: ghrepo.New("OWNER", "REPO"), + resp: []autolink{}, + status: 200, + }, + { + name: "two autolinks", + repo: ghrepo.New("OWNER", "REPO"), + resp: []autolink{ + { + ID: 1, + IsAlphanumeric: true, + KeyPrefix: "key", + URLTemplate: "https://example.com", + }, + { + ID: 2, + IsAlphanumeric: false, + KeyPrefix: "key2", + URLTemplate: "https://example2.com", + }, + }, + status: 200, + }, + { + name: "http error", + repo: ghrepo.New("OWNER", "REPO"), + status: 404, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + reg.Register( + httpmock.REST("GET", fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())), + httpmock.StatusJSONResponse(tt.status, tt.resp), + ) + defer reg.Verify(t) + + autolinkGetter := NewAutolinkGetter(&http.Client{Transport: reg}) + autolinks, err := autolinkGetter.Get(tt.repo) + if tt.status == 404 { + require.Error(t, err) + assert.Equal(t, "error getting autolinks: HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/OWNER/REPO/autolinks)", err.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tt.resp, autolinks) + } + }) + } +} From 869d25193a39e4644311b97bcb6a5a1b33badec0 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 23 Dec 2024 20:26:37 -0500 Subject: [PATCH 0058/2338] Refactor out early return in test code Co-authored-by: Tyler McGoffin --- pkg/cmd/repo/autolink/list_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/repo/autolink/list_test.go b/pkg/cmd/repo/autolink/list_test.go index 4a41f9ad2c4..4b759aaf7a2 100644 --- a/pkg/cmd/repo/autolink/list_test.go +++ b/pkg/cmd/repo/autolink/list_test.go @@ -82,12 +82,11 @@ func TestNewCmdList(t *testing.T) { _, err = cmd.ExecuteC() if tt.wantErr { require.EqualError(t, err, tt.errMsg) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) - assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil) + } else { + require.NoError(t, err) + assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) + assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil) + } }) } } From ea04d2da30767dbd08e4d94f6f47b420f471fbba Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 23 Dec 2024 20:27:11 -0500 Subject: [PATCH 0059/2338] Whitespace --- pkg/cmd/repo/autolink/list_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/repo/autolink/list_test.go b/pkg/cmd/repo/autolink/list_test.go index 4b759aaf7a2..0c24c5ba670 100644 --- a/pkg/cmd/repo/autolink/list_test.go +++ b/pkg/cmd/repo/autolink/list_test.go @@ -83,10 +83,10 @@ func TestNewCmdList(t *testing.T) { if tt.wantErr { require.EqualError(t, err, tt.errMsg) } else { - require.NoError(t, err) - assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) - assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil) - } + require.NoError(t, err) + assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) + assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil) + } }) } } From e98ff2ea387b7d18e2b94f124a0d61c46417f565 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 23 Dec 2024 21:09:10 -0500 Subject: [PATCH 0060/2338] Refactor autolink subcommands into their own packages --- pkg/cmd/repo/autolink/autolink.go | 3 ++- pkg/cmd/repo/autolink/{ => list}/http.go | 2 +- pkg/cmd/repo/autolink/{ => list}/http_test.go | 2 +- pkg/cmd/repo/autolink/{ => list}/list.go | 15 +++++++++++++-- pkg/cmd/repo/autolink/{ => list}/list_test.go | 4 ++-- pkg/cmd/repo/autolink/shared.go | 14 -------------- 6 files changed, 19 insertions(+), 21 deletions(-) rename pkg/cmd/repo/autolink/{ => list}/http.go (98%) rename pkg/cmd/repo/autolink/{ => list}/http_test.go (99%) rename pkg/cmd/repo/autolink/{ => list}/list.go (88%) rename pkg/cmd/repo/autolink/{ => list}/list_test.go (98%) delete mode 100644 pkg/cmd/repo/autolink/shared.go diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index 9c0972f7c3a..d9430f562d4 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -2,6 +2,7 @@ package autolink import ( "github.com/MakeNowJust/heredoc" + cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -22,7 +23,7 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { } cmdutil.EnableRepoOverride(cmd, f) - cmd.AddCommand(newCmdList(f, nil)) + cmd.AddCommand(cmdList.NewCmdList(f, nil)) return cmd } diff --git a/pkg/cmd/repo/autolink/http.go b/pkg/cmd/repo/autolink/list/http.go similarity index 98% rename from pkg/cmd/repo/autolink/http.go rename to pkg/cmd/repo/autolink/list/http.go index a10e98f7bb6..e3fb27e7b9e 100644 --- a/pkg/cmd/repo/autolink/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -1,4 +1,4 @@ -package autolink +package list import ( "encoding/json" diff --git a/pkg/cmd/repo/autolink/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go similarity index 99% rename from pkg/cmd/repo/autolink/http_test.go rename to pkg/cmd/repo/autolink/list/http_test.go index 64ffc1e404f..8a2d5c2963d 100644 --- a/pkg/cmd/repo/autolink/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -1,4 +1,4 @@ -package autolink +package list import ( "fmt" diff --git a/pkg/cmd/repo/autolink/list.go b/pkg/cmd/repo/autolink/list/list.go similarity index 88% rename from pkg/cmd/repo/autolink/list.go rename to pkg/cmd/repo/autolink/list/list.go index 41d0d726639..9db044668fa 100644 --- a/pkg/cmd/repo/autolink/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -1,4 +1,4 @@ -package autolink +package list import ( "fmt" @@ -21,6 +21,17 @@ var autolinkFields = []string{ "urlTemplate", } +type autolink struct { + ID int `json:"id"` + IsAlphanumeric bool `json:"is_alphanumeric"` + KeyPrefix string `json:"key_prefix"` + URLTemplate string `json:"url_template"` +} + +func (s *autolink) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(s, fields) +} + type listOptions struct { BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser @@ -35,7 +46,7 @@ type AutolinkClient interface { Get(repo ghrepo.Interface) ([]autolink, error) } -func newCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { +func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { opts := &listOptions{ Browser: f.Browser, IO: f.IOStreams, diff --git a/pkg/cmd/repo/autolink/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go similarity index 98% rename from pkg/cmd/repo/autolink/list_test.go rename to pkg/cmd/repo/autolink/list/list_test.go index 0c24c5ba670..5db76703220 100644 --- a/pkg/cmd/repo/autolink/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -1,4 +1,4 @@ -package autolink +package list import ( "bytes" @@ -69,7 +69,7 @@ func TestNewCmdList(t *testing.T) { require.NoError(t, err) var gotOpts *listOptions - cmd := newCmdList(f, func(opts *listOptions) error { + cmd := NewCmdList(f, func(opts *listOptions) error { gotOpts = opts return nil }) diff --git a/pkg/cmd/repo/autolink/shared.go b/pkg/cmd/repo/autolink/shared.go deleted file mode 100644 index c8478b346fb..00000000000 --- a/pkg/cmd/repo/autolink/shared.go +++ /dev/null @@ -1,14 +0,0 @@ -package autolink - -import "github.com/cli/cli/v2/pkg/cmdutil" - -type autolink struct { - ID int `json:"id"` - IsAlphanumeric bool `json:"is_alphanumeric"` - KeyPrefix string `json:"key_prefix"` - URLTemplate string `json:"url_template"` -} - -func (s *autolink) ExportData(fields []string) map[string]interface{} { - return cmdutil.StructExportData(s, fields) -} From 67266e9cb883629a0cbdd21b630497b318c8c6ad Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 27 Dec 2024 21:40:52 -0500 Subject: [PATCH 0061/2338] PR nits --- pkg/cmd/repo/autolink/list/http.go | 4 ++-- pkg/cmd/repo/autolink/list/http_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index e3fb27e7b9e..269775883de 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -35,8 +35,8 @@ func (a *AutolinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { } defer resp.Body.Close() - if resp.StatusCode == 404 { - return nil, fmt.Errorf("error getting autolinks: HTTP 404: Must have admin rights to Repository. (https://api.github.com/%s)", path) + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("error getting autolinks: 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) } diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index 8a2d5c2963d..77923ed11b9 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -69,7 +69,7 @@ func TestAutoLinkGetter_Get(t *testing.T) { autolinks, err := autolinkGetter.Get(tt.repo) if tt.status == 404 { require.Error(t, err) - assert.Equal(t, "error getting autolinks: HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/OWNER/REPO/autolinks)", err.Error()) + 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 { require.NoError(t, err) assert.Equal(t, tt.resp, autolinks) From cc2428983228b4ae4f1fd522a13cbe0cd891cc01 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 27 Dec 2024 21:43:47 -0500 Subject: [PATCH 0062/2338] Break out autolink list json fields test --- pkg/cmd/repo/autolink/list/list_test.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 5db76703220..0d9562d6da1 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -11,11 +11,21 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/jsonfieldstest" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestJSONFields(t *testing.T) { + jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdList, []string{ + "id", + "isAlphanumeric", + "keyPrefix", + "urlTemplate", + }) +} + func TestNewCmdList(t *testing.T) { tests := []struct { name string @@ -41,18 +51,6 @@ func TestNewCmdList(t *testing.T) { output: listOptions{}, wantExporter: true, }, - { - name: "invalid json flag", - input: "--json invalid", - wantErr: true, - errMsg: heredoc.Doc(` - Unknown JSON field: "invalid" - Available fields: - id - isAlphanumeric - keyPrefix - urlTemplate`), - }, } for _, tt := range tests { From dc6320f7f7e274f44de8e458e1eb808f851240c9 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 27 Dec 2024 21:47:27 -0500 Subject: [PATCH 0063/2338] Remove NewAutolinkClient --- pkg/cmd/repo/autolink/list/http.go | 10 ++-------- pkg/cmd/repo/autolink/list/http_test.go | 6 ++++-- pkg/cmd/repo/autolink/list/list.go | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index 269775883de..8ce2ba16648 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -12,13 +12,7 @@ import ( ) type AutolinkGetter struct { - HttpClient *http.Client -} - -func NewAutolinkGetter(httpClient *http.Client) *AutolinkGetter { - return &AutolinkGetter{ - HttpClient: httpClient, - } + HTTPClient *http.Client } func (a *AutolinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { @@ -29,7 +23,7 @@ func (a *AutolinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { return nil, err } - resp, err := a.HttpClient.Do(req) + resp, err := a.HTTPClient.Do(req) if err != nil { return nil, err } diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index 77923ed11b9..f75d7e473a6 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -13,7 +13,7 @@ import ( func TestNewAutolinkGetter(t *testing.T) { httpClient := &http.Client{} - autolinkGetter := NewAutolinkGetter(httpClient) + autolinkGetter := &AutolinkGetter{HTTPClient: httpClient} assert.NotNil(t, autolinkGetter) } @@ -65,7 +65,9 @@ func TestAutoLinkGetter_Get(t *testing.T) { ) defer reg.Verify(t) - autolinkGetter := NewAutolinkGetter(&http.Client{Transport: reg}) + autolinkGetter := &AutolinkGetter{ + HTTPClient: &http.Client{Transport: reg}, + } autolinks, err := autolinkGetter.Get(tt.repo) if tt.status == 404 { require.Error(t, err) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 9db044668fa..90c6520441e 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -69,7 +69,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman if err != nil { return err } - opts.AutolinkClient = NewAutolinkGetter(httpClient) + opts.AutolinkClient = &AutolinkGetter{HTTPClient: httpClient} if runF != nil { return runF(opts) From 63488a1a06d1426c0146c14f0d67ccefdc32a982 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 27 Dec 2024 22:02:19 -0500 Subject: [PATCH 0064/2338] Use 'list' instead of 'get' for autolink list type and method --- pkg/cmd/repo/autolink/list/http.go | 4 ++-- pkg/cmd/repo/autolink/list/http_test.go | 6 +++--- pkg/cmd/repo/autolink/list/list.go | 6 +++--- pkg/cmd/repo/autolink/list/list_test.go | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index 8ce2ba16648..b5de22140af 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -11,11 +11,11 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" ) -type AutolinkGetter struct { +type AutolinkLister struct { HTTPClient *http.Client } -func (a *AutolinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { +func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path req, err := http.NewRequest("GET", url, nil) diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index f75d7e473a6..eec74cf3885 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -13,7 +13,7 @@ import ( func TestNewAutolinkGetter(t *testing.T) { httpClient := &http.Client{} - autolinkGetter := &AutolinkGetter{HTTPClient: httpClient} + autolinkGetter := &AutolinkLister{HTTPClient: httpClient} assert.NotNil(t, autolinkGetter) } @@ -65,10 +65,10 @@ func TestAutoLinkGetter_Get(t *testing.T) { ) defer reg.Verify(t) - autolinkGetter := &AutolinkGetter{ + autolinkGetter := &AutolinkLister{ HTTPClient: &http.Client{Transport: reg}, } - autolinks, err := autolinkGetter.Get(tt.repo) + autolinks, err := autolinkGetter.List(tt.repo) if tt.status == 404 { 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()) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 90c6520441e..6f124060530 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -43,7 +43,7 @@ type listOptions struct { } type AutolinkClient interface { - Get(repo ghrepo.Interface) ([]autolink, error) + List(repo ghrepo.Interface) ([]autolink, error) } func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { @@ -69,7 +69,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman if err != nil { return err } - opts.AutolinkClient = &AutolinkGetter{HTTPClient: httpClient} + opts.AutolinkClient = &AutolinkLister{HTTPClient: httpClient} if runF != nil { return runF(opts) @@ -102,7 +102,7 @@ func listRun(opts *listOptions) error { return opts.Browser.Browse(autolinksListURL) } - autolinks, err := opts.AutolinkClient.Get(repo) + autolinks, err := opts.AutolinkClient.List(repo) if err != nil { return err } diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 0d9562d6da1..d6ed6c9cffb 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -93,7 +93,7 @@ type mockAutoLinkGetter struct { Response []autolink } -func (m *mockAutoLinkGetter) Get(repo ghrepo.Interface) ([]autolink, error) { +func (m *mockAutoLinkGetter) List(repo ghrepo.Interface) ([]autolink, error) { return m.Response, nil } @@ -101,7 +101,7 @@ type mockAutoLinkGetterError struct { err error } -func (me *mockAutoLinkGetterError) Get(repo ghrepo.Interface) ([]autolink, error) { +func (me *mockAutoLinkGetterError) List(repo ghrepo.Interface) ([]autolink, error) { return nil, me.err } From 20f086549adff9b437c2de5ab118010da0af5dd2 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 27 Dec 2024 22:03:25 -0500 Subject: [PATCH 0065/2338] Decode instead of unmarshal --- pkg/cmd/repo/autolink/list/http.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index b5de22140af..70d913d70e5 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -3,7 +3,6 @@ package list import ( "encoding/json" "fmt" - "io" "net/http" "github.com/cli/cli/v2/api" @@ -34,14 +33,8 @@ func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) { } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var autolinks []autolink - err = json.Unmarshal(b, &autolinks) + err = json.NewDecoder(resp.Body).Decode(&autolinks) if err != nil { return nil, err } From da826db34227bb8aeb8d7e273a9abafcb6e7e748 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 27 Dec 2024 22:58:12 -0500 Subject: [PATCH 0066/2338] Better error testing for autolink TestListRun --- pkg/cmd/repo/autolink/list/list.go | 1 - pkg/cmd/repo/autolink/list/list_test.go | 170 ++++++++++++++---------- 2 files changed, 97 insertions(+), 74 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 6f124060530..d8a9c9f1292 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -86,7 +86,6 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman } func listRun(opts *listOptions) error { - repo, err := opts.BaseRepo() if err != nil { return err diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index d6ed6c9cffb..e6d0603ef89 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -2,7 +2,6 @@ package list import ( "bytes" - "fmt" "net/http" "testing" @@ -51,6 +50,13 @@ func TestNewCmdList(t *testing.T) { output: listOptions{}, wantExporter: true, }, + { + name: "invalid json flag", + input: "--json invalid", + output: listOptions{}, + wantErr: true, + errMsg: "Unknown JSON field: \"invalid\"\nAvailable fields:\n id\n isAlphanumeric\n keyPrefix\n urlTemplate", + }, } for _, tt := range tests { @@ -89,48 +95,34 @@ func TestNewCmdList(t *testing.T) { } } -type mockAutoLinkGetter struct { - Response []autolink -} - -func (m *mockAutoLinkGetter) List(repo ghrepo.Interface) ([]autolink, error) { - return m.Response, nil -} - -type mockAutoLinkGetterError struct { - err error -} - -func (me *mockAutoLinkGetterError) List(repo ghrepo.Interface) ([]autolink, error) { - return nil, me.err -} - func TestListRun(t *testing.T) { tests := []struct { - name string - opts *listOptions - isTTY bool - response []autolink - wantStdout string - wantStderr string - wantErr bool + name string + opts *listOptions + isTTY bool + stubLister stubAutoLinkLister + expectedErr error + wantStdout string + wantStderr string }{ { name: "list tty", opts: &listOptions{}, isTTY: true, - response: []autolink{ - { - ID: 1, - KeyPrefix: "TICKET-", - URLTemplate: "https://example.com/TICKET?query=", - IsAlphanumeric: true, - }, - { - ID: 2, - KeyPrefix: "STORY-", - URLTemplate: "https://example.com/STORY?id=", - IsAlphanumeric: false, + stubLister: stubAutoLinkLister{ + autolinks: []autolink{ + { + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + { + ID: 2, + KeyPrefix: "STORY-", + URLTemplate: "https://example.com/STORY?id=", + IsAlphanumeric: false, + }, }, }, wantStdout: heredoc.Doc(` @@ -153,18 +145,20 @@ func TestListRun(t *testing.T) { }(), }, isTTY: true, - response: []autolink{ - { - ID: 1, - KeyPrefix: "TICKET-", - URLTemplate: "https://example.com/TICKET?query=", - IsAlphanumeric: true, - }, - { - ID: 2, - KeyPrefix: "STORY-", - URLTemplate: "https://example.com/STORY?id=", - IsAlphanumeric: false, + stubLister: stubAutoLinkLister{ + autolinks: []autolink{ + { + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + { + ID: 2, + KeyPrefix: "STORY-", + URLTemplate: "https://example.com/STORY?id=", + IsAlphanumeric: false, + }, }, }, wantStdout: "[{\"id\":1},{\"id\":2}]\n", @@ -174,18 +168,20 @@ func TestListRun(t *testing.T) { name: "list non-tty", opts: &listOptions{}, isTTY: false, - response: []autolink{ - { - ID: 1, - KeyPrefix: "TICKET-", - URLTemplate: "https://example.com/TICKET?query=", - IsAlphanumeric: true, - }, - { - ID: 2, - KeyPrefix: "STORY-", - URLTemplate: "https://example.com/STORY?id=", - IsAlphanumeric: false, + stubLister: stubAutoLinkLister{ + autolinks: []autolink{ + { + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + { + ID: 2, + KeyPrefix: "STORY-", + URLTemplate: "https://example.com/STORY?id=", + IsAlphanumeric: false, + }, }, }, wantStdout: heredoc.Doc(` @@ -194,15 +190,26 @@ func TestListRun(t *testing.T) { `), wantStderr: "", }, - { - name: "no results", - opts: &listOptions{}, - isTTY: true, - response: []autolink{}, - wantStdout: "", - wantStderr: "", - wantErr: true, + name: "no results", + opts: &listOptions{}, + isTTY: true, + stubLister: stubAutoLinkLister{ + autolinks: []autolink{}, + }, + expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"), + wantStderr: "", + }, + { + name: "client error", + opts: &listOptions{}, + isTTY: true, + stubLister: stubAutoLinkLister{ + autolinks: []autolink{}, + err: testAutolinkClientListError{}, + }, + expectedErr: testAutolinkClientListError{}, + wantStderr: "", }, { name: "web mode", @@ -226,13 +233,13 @@ func TestListRun(t *testing.T) { opts.IO = ios opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } - if tt.wantErr { - opts.AutolinkClient = &mockAutoLinkGetterError{err: fmt.Errorf("mock error")} - err := listRun(opts) + opts.AutolinkClient = &tt.stubLister + err := listRun(opts) + + if tt.expectedErr != nil { require.Error(t, err) + require.ErrorIs(t, err, tt.expectedErr) } else { - opts.AutolinkClient = &mockAutoLinkGetter{Response: tt.response} - err := listRun(opts) require.NoError(t, err) assert.Equal(t, tt.wantStdout, stdout.String()) } @@ -243,3 +250,20 @@ func TestListRun(t *testing.T) { }) } } + +type ( + testAutolinkClientListError struct{} + + stubAutoLinkLister struct { + autolinks []autolink + err error + } +) + +func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) { + return g.autolinks, g.err +} + +func (e testAutolinkClientListError) Error() string { + return "autolink client list error" +} From fa254ba205bd8253178ffec50f89ad405aa969d7 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sat, 28 Dec 2024 07:51:47 -0500 Subject: [PATCH 0067/2338] Complete get -> list renaming --- pkg/cmd/repo/autolink/list/http_test.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index eec74cf3885..fc1e44b238a 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -11,13 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestNewAutolinkGetter(t *testing.T) { - httpClient := &http.Client{} - autolinkGetter := &AutolinkLister{HTTPClient: httpClient} - assert.NotNil(t, autolinkGetter) -} - -func TestAutoLinkGetter_Get(t *testing.T) { +func TestAutoLinkLister_List(t *testing.T) { tests := []struct { name string repo ghrepo.Interface @@ -65,10 +59,10 @@ func TestAutoLinkGetter_Get(t *testing.T) { ) defer reg.Verify(t) - autolinkGetter := &AutolinkLister{ + autolinkLister := &AutolinkLister{ HTTPClient: &http.Client{Transport: reg}, } - autolinks, err := autolinkGetter.List(tt.repo) + autolinks, err := autolinkLister.List(tt.repo) if tt.status == 404 { 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()) From 049332d8448cc8949f5bb2b3f9b8181efe51af4d Mon Sep 17 00:00:00 2001 From: danochoa Date: Mon, 30 Dec 2024 15:41:18 -0600 Subject: [PATCH 0068/2338] gh gist delete: prompt for gist id --- pkg/cmd/gist/delete/delete.go | 38 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index 0b5224c8a74..f9455ac1723 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -8,6 +8,7 @@ import ( "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,6 +19,7 @@ type DeleteOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) HttpClient func() (*http.Client, error) + Prompter prompter.Prompter Selector string } @@ -27,14 +29,17 @@ 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 { | }", Short: "Delete a gist", - Args: cmdutil.ExactArgs(1, "cannot delete: gist argument required"), + Args: cobra.MaximumNArgs(1), RunE: func(c *cobra.Command, args []string) error { - opts.Selector = args[0] + if len(args) == 1 { + opts.Selector = args[0] + } if runF != nil { return runF(&opts) } @@ -46,14 +51,6 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co 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,6 +63,27 @@ func deleteRun(opts *DeleteOptions) error { host, _ := cfg.Authentication().DefaultHost() + cs := opts.IO.ColorScheme() + if gistID == "" { + gistID, err = shared.PromptGists(opts.Prompter, client, host, cs) + if err != nil { + return err + } + + if gistID == "" { + fmt.Fprintln(opts.IO.Out, "No gists found.") + return nil + } + } + + if strings.Contains(gistID, "/") { + id, err := shared.GistIDFromURL(gistID) + if err != nil { + return err + } + gistID = id + } + apiClient := api.NewClientFromHTTP(client) if err := deleteGist(apiClient, host, gistID); err != nil { if errors.Is(err, shared.NotFoundErr) { From 8f19d3b102b54fad2b7c9fe862b6eaea81203dc6 Mon Sep 17 00:00:00 2001 From: danochoa Date: Mon, 30 Dec 2024 17:24:10 -0600 Subject: [PATCH 0069/2338] add gist delete tests for blank gist id --- pkg/cmd/gist/delete/delete_test.go | 63 +++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 00d9ab9618b..bc607d578af 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -2,11 +2,14 @@ package delete import ( "bytes" + "fmt" "net/http" "testing" + "time" "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/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" @@ -27,6 +30,13 @@ func TestNewCmdDelete(t *testing.T) { Selector: "123", }, }, + { + name: "no ID supplied", + cli: "", + wants: DeleteOptions{ + Selector: "", + }, + }, } for _, tt := range tests { @@ -56,12 +66,13 @@ func TestNewCmdDelete(t *testing.T) { 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 + httpStubs func(*httpmock.Registry) + wantErr bool + wantStdout string + wantStderr string + mockGistList bool }{ { name: "successfully delete", @@ -76,6 +87,20 @@ func Test_deleteRun(t *testing.T) { wantStdout: "", wantStderr: "", }, + { + name: "successfully delete with prompt", + opts: DeleteOptions{ + Selector: "", + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("DELETE", "gists/1234"), + httpmock.StatusStringResponse(200, "{}")) + }, + mockGistList: true, + wantErr: false, + wantStdout: "", + wantStderr: "", + }, { name: "not found", opts: DeleteOptions{ @@ -97,6 +122,32 @@ func Test_deleteRun(t *testing.T) { tt.httpStubs(reg) } + 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": [ + { + "name": "1234", + "files": [{ "name": "cool.txt" }], + "description": "", + "updatedAt": "%s", + "isPublic": true + } + ] } } } }`, + sixHoursAgo.Format(time.RFC3339), + )), + ) + + pm := prompter.NewMockPrompter(t) + pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { + return 0, nil + }) + tt.opts.Prompter = pm + } + tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } From 8390b4154bed77441c1663246ab34844fa4c70fc Mon Sep 17 00:00:00 2001 From: danochoa Date: Mon, 30 Dec 2024 18:18:50 -0600 Subject: [PATCH 0070/2338] rearrange struct vals for consistency --- pkg/cmd/gist/delete/delete_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index bc607d578af..3e346da51b5 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -69,10 +69,10 @@ func Test_deleteRun(t *testing.T) { name string opts DeleteOptions httpStubs func(*httpmock.Registry) + mockGistList bool wantErr bool wantStdout string wantStderr string - mockGistList bool }{ { name: "successfully delete", From 079719f9230cd7ccdc26c23375016272a5d13ac3 Mon Sep 17 00:00:00 2001 From: ChandranshuRao14 Date: Tue, 31 Dec 2024 00:47:05 -0500 Subject: [PATCH 0071/2338] Move api call to editRun --- pkg/cmd/repo/edit/edit.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 8efcea11daa..cacc9420df8 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -163,14 +163,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr } if hasSecurityEdits(opts.Edits) { - apiClient := api.NewClientFromHTTP(opts.HTTPClient) - repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"}) - if err != nil { - return err - } - if !repo.ViewerCanAdminister { - return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features") - } opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts) } @@ -260,6 +252,17 @@ func editRun(ctx context.Context, opts *EditOptions) error { } } + if hasSecurityEdits(opts.Edits) { + apiClient := api.NewClientFromHTTP(opts.HTTPClient) + repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"}) + if err != nil { + return err + } + if !repo.ViewerCanAdminister { + return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features") + } + } + apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName()) body := &bytes.Buffer{} From 61ffbe157de8f85d8f8068ea0e765d567165b3a8 Mon Sep 17 00:00:00 2001 From: danochoa Date: Tue, 31 Dec 2024 01:43:39 -0600 Subject: [PATCH 0072/2338] undo extra move --- pkg/cmd/gist/delete/delete.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index f9455ac1723..a6be7053a22 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -51,6 +51,14 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co 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 @@ -76,14 +84,6 @@ func deleteRun(opts *DeleteOptions) error { } } - if strings.Contains(gistID, "/") { - id, err := shared.GistIDFromURL(gistID) - if err != nil { - return err - } - gistID = id - } - apiClient := api.NewClientFromHTTP(client) if err := deleteGist(apiClient, host, gistID); err != nil { if errors.Is(err, shared.NotFoundErr) { From 8560c24f3fbbd828a07c4fef52ddd6e3bc5abf98 Mon Sep 17 00:00:00 2001 From: Aryan Bhosale <36108149+aryanbhosale@users.noreply.github.com> Date: Wed, 1 Jan 2025 11:56:18 +0530 Subject: [PATCH 0073/2338] fix(repo fork): add non-TTY output when fork is newly created --- pkg/cmd/repo/fork/fork.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 73e2695002b..971adcb4118 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -221,6 +221,8 @@ func forkRun(opts *ForkOptions) error { } else { if connectedToTerminal { fmt.Fprintf(stderr, "%s Created fork %s\n", cs.SuccessIconWithColor(cs.Green), cs.Bold(ghrepo.FullName(forkedRepo))) + } else { + fmt.Fprintf(stderr, "Created fork %s\n", ghrepo.FullName(forkedRepo)) } } From 9558d5b60b190fabb08521518b71bb0a954f0a26 Mon Sep 17 00:00:00 2001 From: nobe4 Date: Thu, 2 Jan 2025 16:53:44 +0100 Subject: [PATCH 0074/2338] docs(repo): make explicit which branch is used when creating a repo This adds a line of documentation in the `gh repo create` command's help specifying which branch for the new repo is selected. --- pkg/cmd/repo/create/create.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index ec7026759b8..6e813fe33d1 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -99,6 +99,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co For language or platform .gitignore templates to use with %[1]s--gitignore%[1]s, . For license keywords to use with %[1]s--license%[1]s, run %[1]sgh repo license list%[1]s or visit . + + The repo is created with the configured repository default branch, see . `, "`"), Example: heredoc.Doc(` # create a repository interactively From f1c3619003b4f22b84ea6a9df4149a906695af0a Mon Sep 17 00:00:00 2001 From: nobe4 Date: Thu, 2 Jan 2025 17:11:59 +0100 Subject: [PATCH 0075/2338] Update pkg/cmd/repo/create/create.go Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/repo/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 6e813fe33d1..ba33156c8a6 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -100,7 +100,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co For license keywords to use with %[1]s--license%[1]s, run %[1]sgh repo license list%[1]s or visit . - The repo is created with the configured repository default branch, see . + The repo is created with the configured repository default branch, see . `, "`"), Example: heredoc.Doc(` # create a repository interactively From 375dbf19da99c17b6ce6a130794c4719de5c416d Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 2 Jan 2025 10:08:28 -0800 Subject: [PATCH 0076/2338] Add mention of classic token in gh auth login docs --- pkg/cmd/auth/login/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 8f35972c623..58cbe903808 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -63,7 +63,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm flag. The default authentication mode is a web-based browser flow. After completion, an - authentication token will be stored securely in the system credential store. + authentication token (classic) will be stored securely in the system credential store. If a credential store is not found or there is an issue using it gh will fallback to writing the token to a plain text file. See %[1]sgh auth status%[1]s for its stored location. From a5cf3751cde5278fa76a5ff6c4d2a38a97a21dd4 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Thu, 2 Jan 2025 13:14:23 -0500 Subject: [PATCH 0077/2338] Separate type decrarations --- pkg/cmd/repo/autolink/list/list_test.go | 32 ++++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index e6d0603ef89..3fc8e0261dd 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -95,6 +95,21 @@ func TestNewCmdList(t *testing.T) { } } +type stubAutoLinkLister struct { + autolinks []autolink + err error +} + +func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) { + return g.autolinks, g.err +} + +type testAutolinkClientListError struct{} + +func (e testAutolinkClientListError) Error() string { + return "autolink client list error" +} + func TestListRun(t *testing.T) { tests := []struct { name string @@ -250,20 +265,3 @@ func TestListRun(t *testing.T) { }) } } - -type ( - testAutolinkClientListError struct{} - - stubAutoLinkLister struct { - autolinks []autolink - err error - } -) - -func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) { - return g.autolinks, g.err -} - -func (e testAutolinkClientListError) Error() string { - return "autolink client list error" -} From ae9e68b80356f91e70fda69ee784efbe08ba78d5 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 2 Jan 2025 10:41:25 -0800 Subject: [PATCH 0078/2338] Move mention of classic token to correct line --- pkg/cmd/auth/login/login.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 58cbe903808..225c6ed06f2 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -63,14 +63,16 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm flag. The default authentication mode is a web-based browser flow. After completion, an - authentication token (classic) will be stored securely in the system credential store. + authentication token will be stored securely in the system credential store. If a credential store is not found or there is an issue using it gh will fallback to writing the token to a plain text file. See %[1]sgh auth status%[1]s for its stored location. - Alternatively, use %[1]s--with-token%[1]s to pass in a token on standard input. + Alternatively, use %[1]s--with-token%[1]s to pass in a token (classic) on standard input. The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. + Fine-grained tokens are not supported. + Alternatively, gh will use the authentication token found in environment variables. This method is most suitable for "headless" use of gh such as in automation. See %[1]sgh help environment%[1]s for more info. From aa793f1dac44bd4cfb4b209932993a7288476525 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 2 Jan 2025 14:22:20 -0800 Subject: [PATCH 0079/2338] Update pkg/cmd/auth/login/login.go Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/auth/login/login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 225c6ed06f2..ca165a22fa8 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -68,10 +68,10 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm to writing the token to a plain text file. See %[1]sgh auth status%[1]s for its stored location. - Alternatively, use %[1]s--with-token%[1]s to pass in a token (classic) on standard input. + Alternatively, use %[1]s--with-token%[1]s to pass in a personal access token (classic) on standard input. The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. - Fine-grained tokens are not supported. + Fine-grained personal access tokens are not supported. Alternatively, gh will use the authentication token found in environment variables. This method is most suitable for "headless" use of gh such as in automation. See From 8dbbceaaafbc31d1af9e8c9be1cca58d1e28fa9b Mon Sep 17 00:00:00 2001 From: Aryan Bhosale <36108149+aryanbhosale@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:54:30 +0530 Subject: [PATCH 0080/2338] print repo url to stdout --- pkg/cmd/repo/fork/fork.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 971adcb4118..3d62a73cceb 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -222,7 +222,7 @@ func forkRun(opts *ForkOptions) error { if connectedToTerminal { fmt.Fprintf(stderr, "%s Created fork %s\n", cs.SuccessIconWithColor(cs.Green), cs.Bold(ghrepo.FullName(forkedRepo))) } else { - fmt.Fprintf(stderr, "Created fork %s\n", ghrepo.FullName(forkedRepo)) + fmt.Fprintln(opts.IO.Out, ghrepo.GenerateRepoURL(forkedRepo, "")) } } From 576fa8a3bc97169f24fd5264dc4f4114027308c6 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 2 Jan 2025 22:29:45 -0800 Subject: [PATCH 0081/2338] Add test for permissions check for security and analysis edits (#1) --- pkg/cmd/repo/edit/edit.go | 2 +- pkg/cmd/repo/edit/edit_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index cacc9420df8..daa700cfb68 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -252,7 +252,7 @@ func editRun(ctx context.Context, opts *EditOptions) error { } } - if hasSecurityEdits(opts.Edits) { + if opts.Edits.SecurityAndAnalysis != nil { apiClient := api.NewClientFromHTTP(opts.HTTPClient) repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"}) if err != nil { diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index 93b2564654a..868e300facd 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -220,6 +220,10 @@ func Test_editRun(t *testing.T) { }, }, httpStubs: func(t *testing.T, r *httpmock.Registry) { + r.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(`{"data": { "repository": { "viewerCanAdminister": true } } }`)) + r.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { @@ -231,6 +235,31 @@ func Test_editRun(t *testing.T) { })) }, }, + { + name: "does not have sufficient permissions for security edits", + opts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SecurityAndAnalysis: &SecurityAndAnalysisInput{ + EnableAdvancedSecurity: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanning: &SecurityAndAnalysisStatus{ + Status: sp("enabled"), + }, + EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{ + Status: sp("disabled"), + }, + }, + }, + }, + httpStubs: func(t *testing.T, r *httpmock.Registry) { + r.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(`{"data": { "repository": { "viewerCanAdminister": false } } }`)) + }, + wantsErr: "you do not have sufficient permissions to edit repository security and analysis features", + }, } for _, tt := range tests { From f87d9f98f1ec6570b2f787cd813e310bb3bda0f0 Mon Sep 17 00:00:00 2001 From: danochoa Date: Fri, 3 Jan 2025 12:51:09 -0600 Subject: [PATCH 0082/2338] gist delete prompt for confirmation --- pkg/cmd/gist/delete/delete.go | 41 ++++++++++++++-- pkg/cmd/gist/delete/delete_test.go | 79 +++++++++++++++++++++++------- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index a6be7053a22..85021979bd4 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -6,6 +6,7 @@ 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" @@ -21,7 +22,8 @@ type DeleteOptions struct { 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 { @@ -33,9 +35,23 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd := &cobra.Command{ - Use: "delete { | }", + Use: "delete [ | ]", Short: "Delete a gist", - Args: cobra.MaximumNArgs(1), + 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 { if len(args) == 1 { opts.Selector = args[0] @@ -46,6 +62,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co return deleteRun(&opts) }, } + cmd.Flags().BoolVar(&opts.Confirmed, "yes", false, "confirm deletion without prompting") return cmd } @@ -83,9 +100,16 @@ func deleteRun(opts *DeleteOptions) error { return nil } } + if !opts.Confirmed { + err = opts.Prompter.ConfirmDeletion("delete") + + if err != nil { + return cmdutil.CancelError + } + } apiClient := api.NewClientFromHTTP(client) - if err := deleteGist(apiClient, host, gistID); err != nil { + if err := deleteGist(apiClient, host, gistID, opts); 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) } @@ -95,7 +119,7 @@ func deleteRun(opts *DeleteOptions) error { return nil } -func deleteGist(apiClient *api.Client, hostname string, gistID string) error { +func deleteGist(apiClient *api.Client, hostname string, gistID string, opts *DeleteOptions) error { path := "gists/" + gistID err := apiClient.REST(hostname, "DELETE", path, nil, nil) if err != nil { @@ -105,5 +129,12 @@ func deleteGist(apiClient *api.Client, hostname string, gistID string) error { } return err } + if opts.IO.IsStdoutTTY() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.Out, + "%s Deleted gist %s\n", + cs.SuccessIcon(), + gistID) + } return nil } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 3e346da51b5..c3db5f9405f 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -37,6 +37,13 @@ func TestNewCmdDelete(t *testing.T) { Selector: "", }, }, + { + name: "yes flag", + cli: "123 --yes", + wants: DeleteOptions{ + Selector: "123", + }, + }, } for _, tt := range tests { @@ -67,7 +74,7 @@ func TestNewCmdDelete(t *testing.T) { func Test_deleteRun(t *testing.T) { tests := []struct { name string - opts DeleteOptions + opts *DeleteOptions httpStubs func(*httpmock.Registry) mockGistList bool wantErr bool @@ -76,21 +83,52 @@ func Test_deleteRun(t *testing.T) { }{ { name: "successfully delete", - opts: DeleteOptions{ - Selector: "1234", + opts: &DeleteOptions{ + Selector: "1234", + Confirmed: false, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "", + wantStdout: "✓ Deleted gist 1234\n", wantStderr: "", }, { name: "successfully delete with prompt", - opts: DeleteOptions{ - Selector: "", + opts: &DeleteOptions{ + Selector: "", + Confirmed: false, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("DELETE", "gists/1234"), + httpmock.StatusStringResponse(200, "{}")) + }, + mockGistList: true, + wantErr: false, + wantStdout: "✓ Deleted gist 1234\n", + wantStderr: "", + }, + { + name: "successfully delete with --yes", + opts: &DeleteOptions{ + Selector: "1234", + Confirmed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("DELETE", "gists/1234"), + httpmock.StatusStringResponse(200, "{}")) + }, + wantErr: false, + wantStdout: "✓ Deleted gist 1234\n", + wantStderr: "", + }, + { + name: "successfully delete with prompt and --yes", + opts: &DeleteOptions{ + Selector: "", + Confirmed: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), @@ -98,13 +136,14 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "", + wantStdout: "✓ Deleted gist 1234\n", wantStderr: "", }, { name: "not found", - opts: DeleteOptions{ - Selector: "1234", + opts: &DeleteOptions{ + Selector: "1234", + Confirmed: false, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), @@ -112,14 +151,18 @@ func Test_deleteRun(t *testing.T) { }, wantErr: true, wantStdout: "", - wantStderr: "", + wantStderr: "unable to delete gist 1234: either the gist is not found or it is not owned by you", }, } for _, tt := range tests { reg := &httpmock.Registry{} - if tt.httpStubs != nil { - tt.httpStubs(reg) + tt.httpStubs(reg) + pm := prompter.NewMockPrompter(t) + if !tt.opts.Confirmed { + pm.RegisterConfirmDeletion("Type delete to confirm deletion:", func(_ string) error { + return nil + }) } if tt.mockGistList { @@ -141,12 +184,11 @@ func Test_deleteRun(t *testing.T) { )), ) - pm := prompter.NewMockPrompter(t) pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { return 0, nil }) - tt.opts.Prompter = pm } + tt.opts.Prompter = pm tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil @@ -154,13 +196,13 @@ func Test_deleteRun(t *testing.T) { 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) @@ -170,7 +212,8 @@ 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()) }) } } From 75ace410a3304bcdea9193acc527d8b78d8840a9 Mon Sep 17 00:00:00 2001 From: danochoa Date: Fri, 3 Jan 2025 13:09:32 -0600 Subject: [PATCH 0083/2338] cleanup --- pkg/cmd/gist/delete/delete.go | 6 +----- pkg/cmd/gist/delete/delete_test.go | 9 +++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index 85021979bd4..23e929d5b26 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -94,12 +94,8 @@ func deleteRun(opts *DeleteOptions) error { if err != nil { return err } - - if gistID == "" { - fmt.Fprintln(opts.IO.Out, "No gists found.") - return nil - } } + if !opts.Confirmed { err = opts.Prompter.ConfirmDeletion("delete") diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index c3db5f9405f..fec3b4ffd5e 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -84,8 +84,7 @@ func Test_deleteRun(t *testing.T) { { name: "successfully delete", opts: &DeleteOptions{ - Selector: "1234", - Confirmed: false, + Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), @@ -98,8 +97,7 @@ func Test_deleteRun(t *testing.T) { { name: "successfully delete with prompt", opts: &DeleteOptions{ - Selector: "", - Confirmed: false, + Selector: "", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), @@ -142,8 +140,7 @@ func Test_deleteRun(t *testing.T) { { name: "not found", opts: &DeleteOptions{ - Selector: "1234", - Confirmed: false, + Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), From 7a1052ca339a2475fddd08c22cca1eacd8707251 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:35:48 +0000 Subject: [PATCH 0084/2338] Set LocalBranch even if the git config fails --- .../pr-create-without-upstream-config.txtar | 27 +++++++++++++++++++ git/client.go | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 acceptance/testdata/pr/pr-create-without-upstream-config.txtar diff --git a/acceptance/testdata/pr/pr-create-without-upstream-config.txtar b/acceptance/testdata/pr/pr-create-without-upstream-config.txtar new file mode 100644 index 00000000000..00f3535a775 --- /dev/null +++ b/acceptance/testdata/pr/pr-create-without-upstream-config.txtar @@ -0,0 +1,27 @@ +# This test is the same as pr-create-basic, except that the git push doesn't include the -u argument +# This causes a git config read to fail during gh pr create, but it should not be fatal + +# 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/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private + +# Defer repo cleanup +defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING + +# Clone the repo +exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING + +# Prepare a branch to PR +cd $SCRIPT_NAME-$RANDOM_STRING +exec git checkout -b feature-branch +exec git commit --allow-empty -m 'Empty Commit' +exec git push origin feature-branch + +# Create the PR +exec gh pr create --title 'Feature Title' --body 'Feature Body' + +# Check the PR is indeed created +exec gh pr view +stdout 'Feature Title' diff --git a/git/client.go b/git/client.go index 35b9cf16cdc..774ce5e76a9 100644 --- a/git/client.go +++ b/git/client.go @@ -378,6 +378,8 @@ 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) { + cfg.LocalName = branch + 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...) @@ -389,7 +391,6 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc return } - cfg.LocalName = branch for _, line := range outputLines(out) { parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { From 9d490547b8d20f168e9c665172a3a12204e2f497 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:39:12 +0000 Subject: [PATCH 0085/2338] Alternative: remove LocalBranch from BranchConfig --- git/client.go | 2 -- git/client_test.go | 2 +- git/objects.go | 2 -- pkg/cmd/pr/create/create.go | 6 +++--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/git/client.go b/git/client.go index 774ce5e76a9..1a6d9ae7f08 100644 --- a/git/client.go +++ b/git/client.go @@ -378,8 +378,6 @@ 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) { - cfg.LocalName = branch - 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...) diff --git a/git/client_test.go b/git/client_test.go index 0ec4f733475..fff1397f397 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -737,7 +737,7 @@ func TestClientReadBranchConfig(t *testing.T) { name: "read branch config", 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)$`, - wantBranchConfig: BranchConfig{LocalName: "trunk", RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk"}, + wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk"}, }, } for _, tt := range tests { diff --git a/git/objects.go b/git/objects.go index f371429ddcd..ae058a01cdf 100644 --- a/git/objects.go +++ b/git/objects.go @@ -71,8 +71,6 @@ type Commit struct { } type BranchConfig struct { - // LocalName of the branch. - LocalName string RemoteName string RemoteURL *url.URL // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 3bfa768b7ec..6a1e13849db 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,7 +518,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } -func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, headBranchConfig *git.BranchConfig) *git.TrackingRef { +func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig *git.BranchConfig) *git.TrackingRef { refsForLookup := []string{"HEAD"} var trackingRefs []git.TrackingRef @@ -534,7 +534,7 @@ func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, h for _, remote := range remotes { tr := git.TrackingRef{ RemoteName: remote.Name, - BranchName: headBranchConfig.LocalName, + BranchName: localBranchName, } trackingRefs = append(trackingRefs, tr) refsForLookup = append(refsForLookup, tr.String()) @@ -647,7 +647,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { headBranchConfig := gitClient.ReadBranchConfig(context.Background(), headBranch) if isPushEnabled { // determine whether the head branch is already pushed to a remote - if pushedTo := determineTrackingBranch(gitClient, remotes, &headBranchConfig); pushedTo != nil { + if pushedTo := determineTrackingBranch(gitClient, remotes, headBranch, &headBranchConfig); pushedTo != nil { isPushEnabled = false if r, err := remotes.FindByName(pushedTo.RemoteName); err == nil { headRepo = r From 67749480d566fd2cc268ea68155d132197a5e258 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:45:20 +0000 Subject: [PATCH 0086/2338] Fix test --- pkg/cmd/pr/create/create_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 27220d052ef..9d6584e6cd0 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1719,7 +1719,7 @@ func Test_determineTrackingBranch(t *testing.T) { GitPath: "some/path/git", } headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature") - ref := determineTrackingBranch(gitClient, tt.remotes, &headBranchConfig) + ref := determineTrackingBranch(gitClient, tt.remotes, "feature", &headBranchConfig) tt.assert(ref, t) }) } From efec5d92501b582111254cae679afecb157cc3a2 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 3 Jan 2025 15:50:54 -0800 Subject: [PATCH 0087/2338] Fixed test for stdout in non-tty use case of repo fork --- pkg/cmd/repo/fork/fork_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index 0252602fe26..1f0b9cef1e3 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -390,6 +390,7 @@ func TestRepoFork(t *testing.T) { }, }, httpStubs: forkPost, + wantOut: "https://github.com/someone/REPO\n", }, { name: "implicit nontty remote exists", @@ -424,11 +425,13 @@ func TestRepoFork(t *testing.T) { cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/someone/REPO.git`, 0, "") }, + wantOut: "https://github.com/someone/REPO\n", }, { name: "implicit nontty no args", opts: &ForkOptions{}, httpStubs: forkPost, + wantOut: "https://github.com/someone/REPO\n", }, { name: "passes git flags", @@ -561,6 +564,7 @@ func TestRepoFork(t *testing.T) { Repository: "OWNER/REPO", }, httpStubs: forkPost, + wantOut: "https://github.com/someone/REPO\n", }, { name: "repo arg nontty repo already exists", @@ -604,6 +608,7 @@ func TestRepoFork(t *testing.T) { cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, + wantOut: "https://github.com/someone/REPO\n", }, { name: "non tty repo arg with fork-name", @@ -640,6 +645,7 @@ func TestRepoFork(t *testing.T) { httpmock.StringResponse(renameResult)) }, wantErrOut: "", + wantOut: "https://github.com/OWNER/REPO\n", }, { name: "tty repo arg with fork-name", @@ -694,6 +700,7 @@ func TestRepoFork(t *testing.T) { cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, + wantOut: "https://github.com/someone/REPO\n", }, { name: "does not retry clone if error occurs and exit code is not 128", From 6cf2e9ee3e1b64656bbcd4ef7e4fc0d9681b3378 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 3 Jan 2025 11:14:33 -0500 Subject: [PATCH 0088/2338] feat: Add support for creating autolink references --- pkg/cmd/repo/autolink/autolink.go | 2 + pkg/cmd/repo/autolink/create/create.go | 139 ++++++++++++++ pkg/cmd/repo/autolink/create/create_test.go | 197 ++++++++++++++++++++ pkg/cmd/repo/autolink/create/http.go | 83 +++++++++ pkg/cmd/repo/autolink/create/http_test.go | 155 +++++++++++++++ pkg/cmd/repo/autolink/domain/autolink.go | 14 ++ pkg/cmd/repo/autolink/list/http.go | 7 +- pkg/cmd/repo/autolink/list/http_test.go | 9 +- pkg/cmd/repo/autolink/list/list.go | 18 +- pkg/cmd/repo/autolink/list/list_test.go | 15 +- pkg/httpmock/stub.go | 6 +- 11 files changed, 616 insertions(+), 29 deletions(-) create mode 100644 pkg/cmd/repo/autolink/create/create.go create mode 100644 pkg/cmd/repo/autolink/create/create_test.go create mode 100644 pkg/cmd/repo/autolink/create/http.go create mode 100644 pkg/cmd/repo/autolink/create/http_test.go create mode 100644 pkg/cmd/repo/autolink/domain/autolink.go diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index d9430f562d4..b988de60d0d 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -2,6 +2,7 @@ package autolink import ( "github.com/MakeNowJust/heredoc" + cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create" cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" @@ -24,6 +25,7 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { cmdutil.EnableRepoOverride(cmd, f) cmd.AddCommand(cmdList.NewCmdList(f, nil)) + cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil)) return cmd } diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go new file mode 100644 index 00000000000..698feb555f8 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/create.go @@ -0,0 +1,139 @@ +package create + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type createOptions struct { + BaseRepo func() (ghrepo.Interface, error) + Browser browser.Browser + AutolinkClient AutolinkCreateClient + IO *iostreams.IOStreams + Exporter cmdutil.Exporter + + KeyPrefix string + URLTemplate string + Numeric bool +} + +type AutolinkCreateClient interface { + Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) +} + +func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command { + opts := &createOptions{ + Browser: f.Browser, + IO: f.IOStreams, + } + + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a new autolink reference", + Long: heredoc.Docf(` + Create a new autolink reference for a repository. + + Autolinks automatically generate links to external resources when they appear in an issue, pull request, or commit. + + The %[1]skeyPrefix%[1]s specifies the prefix that will generate a link when it is appended by certain characters. + + The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The urlTemplate must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. + + By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. + + If you are using a shell that applies special meaning to angle brackets, you you will need to need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around this argument. + + Only repository administrators can create autolinks. + `, "`"), + Example: heredoc.Doc(` + # Create an alphanumeric autolink to example.com for the key prefix "TICKET-". This will generate a link to https://example.com/TICKET?query=123abc when "TICKET-123abc" appears. + $ gh repo autolink create TICKET- https://example.com/TICKET?query= + + # Create a numeric autolink to example.com for the key prefix "STORY-". This will generate a link to https://example.com/STORY?id=123 when "STORY-123" appears. + $ gh repo autolink create STORY- https://example.com/STORY?id= --numeric + + `), + Args: cmdutil.ExactArgs(2, "Cannot create autolink: keyPrefix and urlTemplate arguments are both required"), + Aliases: []string{"new"}, + RunE: func(c *cobra.Command, args []string) error { + opts.BaseRepo = f.BaseRepo + httpClient, err := f.HttpClient() + + if err != nil { + return err + } + + opts.AutolinkClient = &AutolinkCreator{HTTPClient: httpClient} + opts.KeyPrefix = args[0] + opts.URLTemplate = args[1] + + if runF != nil { + return runF(opts) + } + + return createRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as non-alphanumeric") + + return cmd +} + +func createRun(opts *createOptions) error { + repo, err := opts.BaseRepo() + if err != nil { + return err + } + cs := opts.IO.ColorScheme() + + isAlphanumeric := !opts.Numeric + + request := AutolinkCreateRequest{ + KeyPrefix: opts.KeyPrefix, + URLTemplate: opts.URLTemplate, + IsAlphanumeric: isAlphanumeric, + } + + autolink, err := opts.AutolinkClient.Create(repo, request) + + if err != nil { + return fmt.Errorf("%s %w", cs.Red("error creating autolink:"), err) + } + + msg := successMsg(autolink, repo, cs) + fmt.Fprint(opts.IO.Out, msg) + + return nil +} + +func successMsg(autolink *domain.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { + autolinkType := "Numeric" + if autolink.IsAlphanumeric { + autolinkType = "Alphanumeric" + } + + createdMsg := fmt.Sprintf( + "%s %s autolink created in %s (id: %d)", + cs.SuccessIconWithColor(cs.Green), + autolinkType, + ghrepo.FullName(repo), + autolink.ID, + ) + + autolinkMapMsg := cs.Bluef( + " %s%s → %s", + autolink.KeyPrefix, + "", + autolink.URLTemplate, + ) + + return fmt.Sprintf("%s\n%s\n", createdMsg, autolinkMapMsg) +} diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go new file mode 100644 index 00000000000..3b31a9c7cb3 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -0,0 +1,197 @@ +package create + +import ( + "bytes" + "fmt" + "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/pkg/cmd/repo/autolink/domain" + "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 TestNewCmdCreate(t *testing.T) { + tests := []struct { + name string + input string + output createOptions + wantErr bool + errMsg string + }{ + { + name: "no argument", + input: "", + wantErr: true, + errMsg: "Cannot create autolink: keyPrefix and urlTemplate arguments are both required", + }, + { + name: "one argument", + input: "TEST-", + wantErr: true, + errMsg: "Cannot create autolink: keyPrefix and urlTemplate arguments are both required", + }, + { + name: "two argument", + input: "TICKET- https://example.com/TICKET?query=", + output: createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + }, + { + name: "numeric flag", + input: "TICKET- https://example.com/TICKET?query= --numeric", + output: createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + Numeric: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + 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 *createOptions + cmd := NewCmdCreate(f, func(opts *createOptions) 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.KeyPrefix, gotOpts.KeyPrefix) + assert.Equal(t, tt.output.URLTemplate, gotOpts.URLTemplate) + assert.Equal(t, tt.output.Numeric, gotOpts.Numeric) + } + }) + } +} + +type stubAutoLinkCreator struct { + err error +} + +func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { + if g.err != nil { + return nil, g.err + } + + return &domain.Autolink{ + ID: 1, + KeyPrefix: request.KeyPrefix, + URLTemplate: request.URLTemplate, + IsAlphanumeric: request.IsAlphanumeric, + }, nil +} + +type testAutolinkClientCreateError struct{} + +func (e testAutolinkClientCreateError) Error() string { + return "autolink client create error" +} + +func TestCreateRun(t *testing.T) { + tests := []struct { + name string + opts *createOptions + stubCreator stubAutoLinkCreator + expectedErr error + errMsg string + wantStdout string + wantStderr string + }{ + { + name: "success, alphanumeric", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubCreator: stubAutoLinkCreator{}, + wantStdout: heredoc.Doc(` + ✓ Alphanumeric autolink created in OWNER/REPO (id: 1) + TICKET- → https://example.com/TICKET?query= + `), + }, + { + name: "success, numeric", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + Numeric: true, + }, + stubCreator: stubAutoLinkCreator{}, + wantStdout: heredoc.Doc(` + ✓ Numeric autolink created in OWNER/REPO (id: 1) + TICKET- → https://example.com/TICKET?query= + `), + }, + { + name: "client error", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubCreator: stubAutoLinkCreator{err: testAutolinkClientCreateError{}}, + expectedErr: testAutolinkClientCreateError{}, + errMsg: fmt.Sprint("error creating autolink: ", testAutolinkClientCreateError{}.Error()), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + + 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.AutolinkClient = &tt.stubCreator + err := createRun(opts) + + if tt.expectedErr != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, err.Error(), tt.errMsg) + } else { + require.NoError(t, err) + } + + if tt.wantStdout != "" { + assert.Equal(t, tt.wantStdout, stdout.String()) + } + + if tt.wantStderr != "" { + assert.Equal(t, tt.wantStderr, stderr.String()) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/create/http.go b/pkg/cmd/repo/autolink/create/http.go new file mode 100644 index 00000000000..e4923c3f02c --- /dev/null +++ b/pkg/cmd/repo/autolink/create/http.go @@ -0,0 +1,83 @@ +package create + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" +) + +type AutolinkCreator struct { + HTTPClient *http.Client +} + +type AutolinkCreateRequest struct { + IsAlphanumeric bool `json:"is_alphanumeric"` + KeyPrefix string `json:"key_prefix"` + URLTemplate string `json:"url_template"` +} + +func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { + path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) + url := ghinstance.RESTPrefix(repo.RepoHost()) + path + + requestByte, err := json.Marshal(request) + if err != nil { + return nil, err + } + requestBody := bytes.NewReader(requestByte) + + req, err := http.NewRequest(http.MethodPost, url, requestBody) + if err != nil { + return nil, err + } + + resp, err := a.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + // if resp.StatusCode != http.StatusCreated { + // return nil, api.HandleHTTPError(resp) + // } + + err = handleAutolinkCreateError(resp) + + if err != nil { + return nil, err + } + + var autolink domain.Autolink + + err = json.NewDecoder(resp.Body).Decode(&autolink) + if err != nil { + return nil, err + } + + return &autolink, nil +} + +func handleAutolinkCreateError(resp *http.Response) error { + switch resp.StatusCode { + case http.StatusCreated: + return nil + case http.StatusNotFound: + err := api.HandleHTTPError(resp) + var httpErr api.HTTPError + if errors.As(err, &httpErr) { + httpErr.Message = "Must have admin rights to Repository." + return httpErr + } + return err + default: + return api.HandleHTTPError(resp) + } +} diff --git a/pkg/cmd/repo/autolink/create/http_test.go b/pkg/cmd/repo/autolink/create/http_test.go new file mode 100644 index 00000000000..7f29f4611d1 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/http_test.go @@ -0,0 +1,155 @@ +package create + +import ( + "fmt" + "net/http" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAutoLinkCreator_Create(t *testing.T) { + repo := ghrepo.New("OWNER", "REPO") + + tests := []struct { + name string + req AutolinkCreateRequest + stubStatus int + stubRespJSON string + + expectedAutolink *domain.Autolink + expectErr bool + expectedErrMsg string + }{ + { + name: "201 successful creation", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubStatus: http.StatusCreated, + stubRespJSON: `{ + "id": 1, + "is_alphanumeric": true, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=" + }`, + expectedAutolink: &domain.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + { + name: "422 URL template not valid URL", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "foo/", + }, + stubStatus: http.StatusUnprocessableEntity, + stubRespJSON: `{ + "message": "Validation Failed", + "errors": [ + { + "resource": "KeyLink", + "code": "custom", + "field": "url_template", + "message": "url_template must be an absolute URL" + } + ], + "documentation_url": "https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository", + "status": "422" + }`, + expectErr: true, + expectedErrMsg: heredoc.Doc(` + HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks) + url_template must be an absolute URL`), + }, + { + name: "404 repo not found", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubStatus: http.StatusNotFound, + stubRespJSON: `{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository", + "status": "404" + }`, + expectErr: true, + expectedErrMsg: "HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/OWNER/REPO/autolinks)", + }, + { + name: "422 URL template missing ", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET", + }, + stubStatus: http.StatusUnprocessableEntity, + stubRespJSON: `{"message":"Validation Failed","errors":[{"resource":"KeyLink","code":"custom","field":"url_template","message":"url_template is missing a token"}],"documentation_url":"https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository","status":"422"}`, + expectErr: true, + expectedErrMsg: heredoc.Doc(` + HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks) + url_template is missing a token`), + }, + { + name: "422 already exists", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubStatus: http.StatusUnprocessableEntity, + stubRespJSON: `{"message":"Validation Failed","errors":[{"resource":"KeyLink","code":"already_exists","field":"key_prefix"}],"documentation_url":"https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository","status":"422"}`, + expectErr: true, + expectedErrMsg: heredoc.Doc(` + HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks) + KeyLink.key_prefix already exists`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + reg.Register( + httpmock.REST( + http.MethodPost, + fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName())), + httpmock.RESTPayload(tt.stubStatus, tt.stubRespJSON, + func(payload map[string]interface{}) { + require.Equal(t, map[string]interface{}{ + "is_alphanumeric": tt.req.IsAlphanumeric, + "key_prefix": tt.req.KeyPrefix, + "url_template": tt.req.URLTemplate, + }, payload) + }, + ), + ) + defer reg.Verify(t) + + autolinkCreator := &AutolinkCreator{ + HTTPClient: &http.Client{Transport: reg}, + } + + autolink, err := autolinkCreator.Create(repo, tt.req) + + if tt.expectErr { + require.EqualError(t, err, tt.expectedErrMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedAutolink, autolink) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/domain/autolink.go b/pkg/cmd/repo/autolink/domain/autolink.go new file mode 100644 index 00000000000..9c3f10c05e1 --- /dev/null +++ b/pkg/cmd/repo/autolink/domain/autolink.go @@ -0,0 +1,14 @@ +package domain + +import "github.com/cli/cli/v2/pkg/cmdutil" + +type Autolink struct { + ID int `json:"id"` + IsAlphanumeric bool `json:"is_alphanumeric"` + KeyPrefix string `json:"key_prefix"` + URLTemplate string `json:"url_template"` +} + +func (a *Autolink) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(a, fields) +} diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index 70d913d70e5..a5e707f4eeb 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -8,16 +8,17 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" ) type AutolinkLister struct { HTTPClient *http.Client } -func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) { +func (a *AutolinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } @@ -33,7 +34,7 @@ func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) { } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } - var autolinks []autolink + var autolinks []domain.Autolink err = json.NewDecoder(resp.Body).Decode(&autolinks) if err != nil { return nil, err diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index fc1e44b238a..46f83484fc6 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,19 +16,19 @@ func TestAutoLinkLister_List(t *testing.T) { tests := []struct { name string repo ghrepo.Interface - resp []autolink + resp []domain.Autolink status int }{ { name: "no autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []autolink{}, + resp: []domain.Autolink{}, status: 200, }, { name: "two autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []autolink{ + resp: []domain.Autolink{ { ID: 1, IsAlphanumeric: true, @@ -54,7 +55,7 @@ func TestAutoLinkLister_List(t *testing.T) { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} reg.Register( - httpmock.REST("GET", fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())), + httpmock.REST(http.MethodGet, fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())), httpmock.StatusJSONResponse(tt.status, tt.resp), ) defer reg.Verify(t) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index d8a9c9f1292..8d4a484e9c0 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -21,29 +22,18 @@ var autolinkFields = []string{ "urlTemplate", } -type autolink struct { - ID int `json:"id"` - IsAlphanumeric bool `json:"is_alphanumeric"` - KeyPrefix string `json:"key_prefix"` - URLTemplate string `json:"url_template"` -} - -func (s *autolink) ExportData(fields []string) map[string]interface{} { - return cmdutil.StructExportData(s, fields) -} - type listOptions struct { BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser - AutolinkClient AutolinkClient + AutolinkClient AutolinkListClient IO *iostreams.IOStreams Exporter cmdutil.Exporter WebMode bool } -type AutolinkClient interface { - List(repo ghrepo.Interface) ([]autolink, error) +type AutolinkListClient interface { + List(repo ghrepo.Interface) ([]domain.Autolink, error) } func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 3fc8e0261dd..0ba350ab05c 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -8,6 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/jsonfieldstest" @@ -96,11 +97,11 @@ func TestNewCmdList(t *testing.T) { } type stubAutoLinkLister struct { - autolinks []autolink + autolinks []domain.Autolink err error } -func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) { +func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { return g.autolinks, g.err } @@ -125,7 +126,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []domain.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -161,7 +162,7 @@ func TestListRun(t *testing.T) { }, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []domain.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -184,7 +185,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: false, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []domain.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -210,7 +211,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{}, + autolinks: []domain.Autolink{}, }, expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"), wantStderr: "", @@ -220,7 +221,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{}, + autolinks: []domain.Autolink{}, err: testAutolinkClientListError{}, }, expectedErr: testAutolinkClientListError{}, diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 196a047d889..4e61d12f44f 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -188,7 +188,11 @@ func RESTPayload(responseStatus int, responseBody string, cb func(payload map[st return nil, err } cb(bodyData) - return httpResponse(responseStatus, req, bytes.NewBufferString(responseBody)), nil + + header := http.Header{ + "Content-Type": []string{"application/json"}, + } + return httpResponseWithHeader(responseStatus, req, bytes.NewBufferString(responseBody), header), nil } } From fdf9a6e2f6ac605904b4bfad5dad1cdfc1729df0 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 5 Jan 2025 19:41:15 -0500 Subject: [PATCH 0089/2338] Fix typos --- pkg/cmd/repo/autolink/create/create.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index 698feb555f8..7c75f135239 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -44,11 +44,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co The %[1]skeyPrefix%[1]s specifies the prefix that will generate a link when it is appended by certain characters. - The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The urlTemplate must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. + The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The %[1]surlTemplate%[1]s must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. - By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. + By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. - If you are using a shell that applies special meaning to angle brackets, you you will need to need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around this argument. + If you are using a shell that applies special meaning to angle brackets, you will need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around the whole argument. Only repository administrators can create autolinks. `, "`"), From dd32a9cc94f61c823e81568c2822cf4432471a70 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Sun, 5 Jan 2025 23:18:28 -0500 Subject: [PATCH 0090/2338] Upgrade generated workflows Use `cli/gh-extension-precompile@v2` Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml | 2 +- pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml b/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml index 080019c2a91..b804448c6e0 100644 --- a/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml +++ b/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cli/gh-extension-precompile@v1 + - uses: cli/gh-extension-precompile@v2 with: generate_attestations: true go_version_file: go.mod diff --git a/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml b/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml index 78ba05171c8..4eb99a4a4d1 100644 --- a/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml +++ b/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cli/gh-extension-precompile@v1 + - uses: cli/gh-extension-precompile@v2 with: build_script_override: "script/build.sh" From 3695bda6379a1444781110acefe1e0b1306eace3 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 16:30:48 +0100 Subject: [PATCH 0091/2338] Clear up --with-token fine grained PAT usage --- pkg/cmd/auth/login/login.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index ca165a22fa8..c8011e597df 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -69,9 +69,10 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm stored location. Alternatively, use %[1]s--with-token%[1]s to pass in a personal access token (classic) on standard input. - The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. - - Fine-grained personal access tokens are not supported. + The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. Although + it is possible to pass a fine-grained personal access token to %[1]s--with-token%[1]s, it should be done with + care, as the inherent scoping to certain resources may cause confusing behaviour when interacting with other + resources. Favour setting %[1]sGH_TOKEN$%[1]s for fine-grained personal access token usage. Alternatively, gh will use the authentication token found in environment variables. This method is most suitable for "headless" use of gh such as in automation. See From e7d49fb78c371ba5df66407dbfa9f3aec6e61ea6 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 17:21:02 +0100 Subject: [PATCH 0092/2338] Update pkg/cmd/auth/login/login.go Co-authored-by: Andy Feller --- pkg/cmd/auth/login/login.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index c8011e597df..347fcdb6a3c 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -69,9 +69,9 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm stored location. Alternatively, use %[1]s--with-token%[1]s to pass in a personal access token (classic) on standard input. - The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. Although - it is possible to pass a fine-grained personal access token to %[1]s--with-token%[1]s, it should be done with - care, as the inherent scoping to certain resources may cause confusing behaviour when interacting with other + The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. + Take care when passing a fine-grained personal access token to %[1]s--with-token%[1]s + as the inherent scoping to certain resources may cause confusing behaviour when interacting with other resources. Favour setting %[1]sGH_TOKEN$%[1]s for fine-grained personal access token usage. Alternatively, gh will use the authentication token found in environment variables. From 3ae4e5da20ebd9f9f46342906c0a930b75310d3b Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 15:14:43 +0100 Subject: [PATCH 0093/2338] Document and rework pr create tracking branch lookup --- git/objects.go | 12 ++++++++++++ pkg/cmd/pr/create/create.go | 30 +++++++++++++++++++----------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/git/objects.go b/git/objects.go index ae058a01cdf..924a3059e3b 100644 --- a/git/objects.go +++ b/git/objects.go @@ -1,6 +1,7 @@ package git import ( + "fmt" "net/url" "strings" ) @@ -64,6 +65,17 @@ func (r TrackingRef) String() string { return "refs/remotes/" + r.RemoteName + "/" + r.BranchName } +func ParseTrackingRef(text string) (TrackingRef, error) { + parts := strings.SplitN(string(text), "/", 4) + if len(parts) != 4 { + return TrackingRef{}, fmt.Errorf("invalid tracking ref: %s", text) + } + return TrackingRef{ + RemoteName: parts[2], + BranchName: parts[3], + }, nil +} + type Commit struct { Sha string Title string diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 6a1e13849db..df62dc88943 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -519,15 +519,14 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u } func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig *git.BranchConfig) *git.TrackingRef { + // To try and determine the tracking ref for a local branch, we first construct a collection of refs + // that might be tracking, given the current branch's config, and the list of known remotes. refsForLookup := []string{"HEAD"} - var trackingRefs []git.TrackingRef - - if headBranchConfig.RemoteName != "" { + if headBranchConfig.RemoteName != "" && headBranchConfig.MergeRef != "" { tr := git.TrackingRef{ RemoteName: headBranchConfig.RemoteName, BranchName: strings.TrimPrefix(headBranchConfig.MergeRef, "refs/heads/"), } - trackingRefs = append(trackingRefs, tr) refsForLookup = append(refsForLookup, tr.String()) } @@ -536,22 +535,31 @@ func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, l RemoteName: remote.Name, BranchName: localBranchName, } - trackingRefs = append(trackingRefs, tr) refsForLookup = append(refsForLookup, tr.String()) } + // Then we ask git for details about these refs, for example, refs/remotes/origin/trunk might return a hash + // for the remote tracking branch, trunk, for the remote, origin. If there is no ref, the git client returns + // no ref information. + // + // We also first check for the HEAD ref, so that we have the hash of the currently checked out commit. resolvedRefs, _ := gitClient.ShowRefs(context.Background(), refsForLookup) + + // If there is more than one resolved ref, that means that at least one ref was found in addition to the HEAD. if len(resolvedRefs) > 1 { + headRef := resolvedRefs[0] for _, r := range resolvedRefs[1:] { - if r.Hash != resolvedRefs[0].Hash { + // If the hash of the remote ref doesn't match the hash of HEAD then the remote branch is not in the same + // state, so it can't be used. + if r.Hash != headRef.Hash { continue } - for _, tr := range trackingRefs { - if tr.String() != r.Name { - continue - } - return &tr + // Otherwise we can parse the returned ref into a tracking ref and return that + trackingRef, err := git.ParseTrackingRef(r.Name) + if err != nil { + return nil } + return &trackingRef } } From dc077dc09ba68843e973b715c448e376135e6c67 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 15:44:34 +0100 Subject: [PATCH 0094/2338] Panic if tracking ref can't be reconstructed --- git/objects.go | 12 ------------ pkg/cmd/pr/create/create.go | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/git/objects.go b/git/objects.go index 924a3059e3b..ae058a01cdf 100644 --- a/git/objects.go +++ b/git/objects.go @@ -1,7 +1,6 @@ package git import ( - "fmt" "net/url" "strings" ) @@ -65,17 +64,6 @@ func (r TrackingRef) String() string { return "refs/remotes/" + r.RemoteName + "/" + r.BranchName } -func ParseTrackingRef(text string) (TrackingRef, error) { - parts := strings.SplitN(string(text), "/", 4) - if len(parts) != 4 { - return TrackingRef{}, fmt.Errorf("invalid tracking ref: %s", text) - } - return TrackingRef{ - RemoteName: parts[2], - BranchName: parts[3], - }, nil -} - type Commit struct { Sha string Title string diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index df62dc88943..d7032714ac2 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -555,10 +555,7 @@ func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, l continue } // Otherwise we can parse the returned ref into a tracking ref and return that - trackingRef, err := git.ParseTrackingRef(r.Name) - if err != nil { - return nil - } + trackingRef := mustParseTrackingRef(r.Name) return &trackingRef } } @@ -566,6 +563,17 @@ func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, l return nil } +func mustParseTrackingRef(text string) git.TrackingRef { + parts := strings.SplitN(string(text), "/", 4) + if len(parts) != 4 { + panic(fmt.Errorf("invalid tracking ref: %s", text)) + } + return git.TrackingRef{ + RemoteName: parts[2], + BranchName: parts[3], + } +} + func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadataState, error) { var milestoneTitles []string if opts.Milestone != "" { From 05764b8114e59411cf41723124d83fbb400d9395 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 15:47:10 +0100 Subject: [PATCH 0095/2338] Don't use pointer for determineTrackingBranch branchConfig --- pkg/cmd/pr/create/create.go | 4 ++-- pkg/cmd/pr/create/create_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index d7032714ac2..40913db6425 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,7 +518,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } -func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig *git.BranchConfig) *git.TrackingRef { +func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) *git.TrackingRef { // To try and determine the tracking ref for a local branch, we first construct a collection of refs // that might be tracking, given the current branch's config, and the list of known remotes. refsForLookup := []string{"HEAD"} @@ -663,7 +663,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { headBranchConfig := gitClient.ReadBranchConfig(context.Background(), headBranch) if isPushEnabled { // determine whether the head branch is already pushed to a remote - if pushedTo := determineTrackingBranch(gitClient, remotes, headBranch, &headBranchConfig); pushedTo != nil { + if pushedTo := determineTrackingBranch(gitClient, remotes, headBranch, headBranchConfig); pushedTo != nil { isPushEnabled = false if r, err := remotes.FindByName(pushedTo.RemoteName); err == nil { headRepo = r diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 9d6584e6cd0..8d5a1d1b33c 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1719,7 +1719,7 @@ func Test_determineTrackingBranch(t *testing.T) { GitPath: "some/path/git", } headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature") - ref := determineTrackingBranch(gitClient, tt.remotes, "feature", &headBranchConfig) + ref := determineTrackingBranch(gitClient, tt.remotes, "feature", headBranchConfig) tt.assert(ref, t) }) } From 27bd4b2aec09f1242177821ad72a01b01163c998 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 15:49:53 +0100 Subject: [PATCH 0096/2338] Doc determineTrackingBranch --- pkg/cmd/pr/create/create.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 40913db6425..b05c971fe15 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,6 +518,8 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } +// determineTrackingBranch is intended to try and find a remote branch on the same commit as the currently checked out +// HEAD, i.e. the local branch. func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) *git.TrackingRef { // To try and determine the tracking ref for a local branch, we first construct a collection of refs // that might be tracking, given the current branch's config, and the list of known remotes. From b8c167970b70205cba2fd22e2bb2da378229f0d7 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 15:55:35 +0100 Subject: [PATCH 0097/2338] Avoid pointer return from determineTrackingBranch --- pkg/cmd/pr/create/create.go | 17 ++++++++-------- pkg/cmd/pr/create/create_test.go | 33 +++++++++++++++++++------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index b05c971fe15..00993e0fa90 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,9 +518,9 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } -// determineTrackingBranch is intended to try and find a remote branch on the same commit as the currently checked out +// tryDetermineTrackingRef is intended to try and find a remote branch on the same commit as the currently checked out // HEAD, i.e. the local branch. -func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) *git.TrackingRef { +func tryDetermineTrackingRef(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) (git.TrackingRef, bool) { // To try and determine the tracking ref for a local branch, we first construct a collection of refs // that might be tracking, given the current branch's config, and the list of known remotes. refsForLookup := []string{"HEAD"} @@ -557,12 +557,11 @@ func determineTrackingBranch(gitClient *git.Client, remotes ghContext.Remotes, l continue } // Otherwise we can parse the returned ref into a tracking ref and return that - trackingRef := mustParseTrackingRef(r.Name) - return &trackingRef + return mustParseTrackingRef(r.Name), true } } - return nil + return git.TrackingRef{}, false } func mustParseTrackingRef(text string) git.TrackingRef { @@ -665,14 +664,14 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { headBranchConfig := gitClient.ReadBranchConfig(context.Background(), headBranch) if isPushEnabled { // determine whether the head branch is already pushed to a remote - if pushedTo := determineTrackingBranch(gitClient, remotes, headBranch, headBranchConfig); pushedTo != nil { + if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found { isPushEnabled = false - if r, err := remotes.FindByName(pushedTo.RemoteName); err == nil { + if r, err := remotes.FindByName(trackingRef.RemoteName); err == nil { headRepo = r headRemote = r - headBranchLabel = pushedTo.BranchName + headBranchLabel = trackingRef.BranchName if !ghrepo.IsSame(baseRepo, headRepo) { - headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), pushedTo.BranchName) + headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), trackingRef.BranchName) } } } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 8d5a1d1b33c..acb35f5576e 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1622,12 +1622,12 @@ func Test_createRun(t *testing.T) { } } -func Test_determineTrackingBranch(t *testing.T) { +func Test_tryDetermineTrackingRef(t *testing.T) { tests := []struct { name string cmdStubs func(*run.CommandStubber) remotes context.Remotes - assert func(ref *git.TrackingRef, t *testing.T) + assert func(ref git.TrackingRef, found bool, t *testing.T) }{ { name: "empty", @@ -1635,8 +1635,9 @@ func Test_determineTrackingBranch(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, - assert: func(ref *git.TrackingRef, t *testing.T) { - assert.Nil(t, ref) + assert: func(ref git.TrackingRef, found bool, t *testing.T) { + assert.Zero(t, ref) + assert.False(t, found) }, }, { @@ -1655,8 +1656,9 @@ func Test_determineTrackingBranch(t *testing.T) { Repo: ghrepo.New("octocat", "Spoon-Knife"), }, }, - assert: func(ref *git.TrackingRef, t *testing.T) { - assert.Nil(t, ref) + assert: func(ref git.TrackingRef, found bool, t *testing.T) { + assert.Zero(t, ref) + assert.False(t, found) }, }, { @@ -1679,9 +1681,12 @@ func Test_determineTrackingBranch(t *testing.T) { Repo: ghrepo.New("octocat", "Spoon-Knife"), }, }, - assert: func(ref *git.TrackingRef, t *testing.T) { - assert.Equal(t, "upstream", ref.RemoteName) - assert.Equal(t, "feature", ref.BranchName) + assert: func(ref git.TrackingRef, found bool, t *testing.T) { + assert.Equal(t, git.TrackingRef{ + RemoteName: "upstream", + BranchName: "feature", + }, ref) + assert.True(t, found) }, }, { @@ -1702,8 +1707,9 @@ func Test_determineTrackingBranch(t *testing.T) { Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, - assert: func(ref *git.TrackingRef, t *testing.T) { - assert.Nil(t, ref) + assert: func(ref git.TrackingRef, found bool, t *testing.T) { + assert.Zero(t, ref) + assert.False(t, found) }, }, } @@ -1719,8 +1725,9 @@ func Test_determineTrackingBranch(t *testing.T) { GitPath: "some/path/git", } headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature") - ref := determineTrackingBranch(gitClient, tt.remotes, "feature", headBranchConfig) - tt.assert(ref, t) + ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", headBranchConfig) + + tt.assert(ref, found, t) }) } } From 57ba5e56082e442e3e4e856ba8ca4aec703c8499 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 15:57:14 +0100 Subject: [PATCH 0098/2338] Rework tryDetermineTrackingRef tests --- pkg/cmd/pr/create/create_test.go | 40 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index acb35f5576e..4785a9d99fd 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1624,10 +1624,11 @@ func Test_createRun(t *testing.T) { func Test_tryDetermineTrackingRef(t *testing.T) { tests := []struct { - name string - cmdStubs func(*run.CommandStubber) - remotes context.Remotes - assert func(ref git.TrackingRef, found bool, t *testing.T) + name string + cmdStubs func(*run.CommandStubber) + remotes context.Remotes + expectedTrackingRef git.TrackingRef + expectedFound bool }{ { name: "empty", @@ -1635,10 +1636,8 @@ func Test_tryDetermineTrackingRef(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, - assert: func(ref git.TrackingRef, found bool, t *testing.T) { - assert.Zero(t, ref) - assert.False(t, found) - }, + expectedTrackingRef: git.TrackingRef{}, + expectedFound: false, }, { name: "no match", @@ -1656,10 +1655,8 @@ func Test_tryDetermineTrackingRef(t *testing.T) { Repo: ghrepo.New("octocat", "Spoon-Knife"), }, }, - assert: func(ref git.TrackingRef, found bool, t *testing.T) { - assert.Zero(t, ref) - assert.False(t, found) - }, + expectedTrackingRef: git.TrackingRef{}, + expectedFound: false, }, { name: "match", @@ -1681,13 +1678,11 @@ func Test_tryDetermineTrackingRef(t *testing.T) { Repo: ghrepo.New("octocat", "Spoon-Knife"), }, }, - assert: func(ref git.TrackingRef, found bool, t *testing.T) { - assert.Equal(t, git.TrackingRef{ - RemoteName: "upstream", - BranchName: "feature", - }, ref) - assert.True(t, found) + expectedTrackingRef: git.TrackingRef{ + RemoteName: "upstream", + BranchName: "feature", }, + expectedFound: true, }, { name: "respect tracking config", @@ -1707,10 +1702,8 @@ func Test_tryDetermineTrackingRef(t *testing.T) { Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, - assert: func(ref git.TrackingRef, found bool, t *testing.T) { - assert.Zero(t, ref) - assert.False(t, found) - }, + expectedTrackingRef: git.TrackingRef{}, + expectedFound: false, }, } for _, tt := range tests { @@ -1727,7 +1720,8 @@ func Test_tryDetermineTrackingRef(t *testing.T) { headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature") ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", headBranchConfig) - tt.assert(ref, found, t) + assert.Equal(t, tt.expectedTrackingRef, ref) + assert.Equal(t, tt.expectedFound, found) }) } } From 62ecb1c84dbb37ea908fa25c7f5e11a27faaf51c Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 16:10:12 +0100 Subject: [PATCH 0099/2338] Make tryDetermineTrackingRef tests more respective of reality Though it doesn't really matter, in practice upstream is always going to come before origin. --- pkg/cmd/pr/create/create.go | 4 +++- pkg/cmd/pr/create/create_test.go | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 00993e0fa90..372daf86c1b 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -519,7 +519,9 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u } // tryDetermineTrackingRef is intended to try and find a remote branch on the same commit as the currently checked out -// HEAD, i.e. the local branch. +// HEAD, i.e. the local branch. If there are multiple branches that might match, the first remote is chosen, which in +// practice is determined by the sorting algorithm applied much earlier in the process, roughly "upstream", "github", "origin", +// and then everything else unstably sorted. func tryDetermineTrackingRef(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) (git.TrackingRef, bool) { // To try and determine the tracking ref for a local branch, we first construct a collection of refs // that might be tracking, given the current branch's config, and the list of known remotes. diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 4785a9d99fd..c5127bcc6bc 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1643,17 +1643,17 @@ func Test_tryDetermineTrackingRef(t *testing.T) { 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/origin/feature refs/remotes/upstream/feature", 0, "abc HEAD\nbca refs/remotes/origin/feature") + cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature") }, remotes: context.Remotes{ - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("hubot", "Spoon-Knife"), - }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, Repo: ghrepo.New("octocat", "Spoon-Knife"), }, + &context.Remote{ + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.New("hubot", "Spoon-Knife"), + }, }, expectedTrackingRef: git.TrackingRef{}, expectedFound: false, @@ -1662,24 +1662,24 @@ 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/origin/feature refs/remotes/upstream/feature$`, 0, heredoc.Doc(` + cs.Register(`git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature$`, 0, heredoc.Doc(` deadbeef HEAD - deadb00f refs/remotes/origin/feature - deadbeef refs/remotes/upstream/feature + deadb00f refs/remotes/upstream/feature + deadbeef refs/remotes/origin/feature `)) }, remotes: context.Remotes{ - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("hubot", "Spoon-Knife"), - }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, Repo: ghrepo.New("octocat", "Spoon-Knife"), }, + &context.Remote{ + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.New("hubot", "Spoon-Knife"), + }, }, expectedTrackingRef: git.TrackingRef{ - RemoteName: "upstream", + RemoteName: "origin", BranchName: "feature", }, expectedFound: true, From 8b5073d6172f9cfe4ad4c8ba157ee35bff3f6ed6 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 16:58:35 +0100 Subject: [PATCH 0100/2338] Move trackingRef into pr create package --- git/objects.go | 10 ------ pkg/cmd/pr/create/create.go | 54 +++++++++++++++++++------------- pkg/cmd/pr/create/create_test.go | 14 ++++----- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/git/objects.go b/git/objects.go index ae058a01cdf..c096830425e 100644 --- a/git/objects.go +++ b/git/objects.go @@ -54,16 +54,6 @@ type Ref struct { Name string } -// TrackingRef represents a ref for a remote tracking branch. -type TrackingRef struct { - RemoteName string - BranchName string -} - -func (r TrackingRef) String() string { - return "refs/remotes/" + r.RemoteName + "/" + r.BranchName -} - type Commit struct { Sha string Title string diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 372daf86c1b..46a4e284e2c 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,26 +518,47 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } +// trackingRef represents a ref for a remote tracking branch. +type trackingRef struct { + remoteName string + branchName string +} + +func (r trackingRef) String() string { + return "refs/remotes/" + r.remoteName + "/" + r.branchName +} + +func mustParseTrackingRef(text string) trackingRef { + parts := strings.SplitN(string(text), "/", 4) + if len(parts) != 4 { + panic(fmt.Errorf("invalid tracking ref: %s", text)) + } + return trackingRef{ + remoteName: parts[2], + branchName: parts[3], + } +} + // tryDetermineTrackingRef is intended to try and find a remote branch on the same commit as the currently checked out // HEAD, i.e. the local branch. If there are multiple branches that might match, the first remote is chosen, which in // practice is determined by the sorting algorithm applied much earlier in the process, roughly "upstream", "github", "origin", // and then everything else unstably sorted. -func tryDetermineTrackingRef(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) (git.TrackingRef, bool) { +func tryDetermineTrackingRef(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) (trackingRef, bool) { // To try and determine the tracking ref for a local branch, we first construct a collection of refs // that might be tracking, given the current branch's config, and the list of known remotes. refsForLookup := []string{"HEAD"} if headBranchConfig.RemoteName != "" && headBranchConfig.MergeRef != "" { - tr := git.TrackingRef{ - RemoteName: headBranchConfig.RemoteName, - BranchName: strings.TrimPrefix(headBranchConfig.MergeRef, "refs/heads/"), + tr := trackingRef{ + remoteName: headBranchConfig.RemoteName, + branchName: strings.TrimPrefix(headBranchConfig.MergeRef, "refs/heads/"), } refsForLookup = append(refsForLookup, tr.String()) } for _, remote := range remotes { - tr := git.TrackingRef{ - RemoteName: remote.Name, - BranchName: localBranchName, + tr := trackingRef{ + remoteName: remote.Name, + branchName: localBranchName, } refsForLookup = append(refsForLookup, tr.String()) } @@ -563,18 +584,7 @@ func tryDetermineTrackingRef(gitClient *git.Client, remotes ghContext.Remotes, l } } - return git.TrackingRef{}, false -} - -func mustParseTrackingRef(text string) git.TrackingRef { - parts := strings.SplitN(string(text), "/", 4) - if len(parts) != 4 { - panic(fmt.Errorf("invalid tracking ref: %s", text)) - } - return git.TrackingRef{ - RemoteName: parts[2], - BranchName: parts[3], - } + return trackingRef{}, false } func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadataState, error) { @@ -668,12 +678,12 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { // determine whether the head branch is already pushed to a remote if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found { isPushEnabled = false - if r, err := remotes.FindByName(trackingRef.RemoteName); err == nil { + if r, err := remotes.FindByName(trackingRef.remoteName); err == nil { headRepo = r headRemote = r - headBranchLabel = trackingRef.BranchName + headBranchLabel = trackingRef.branchName if !ghrepo.IsSame(baseRepo, headRepo) { - headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), trackingRef.BranchName) + headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), trackingRef.branchName) } } } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index c5127bcc6bc..6c0ff11e226 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1627,7 +1627,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { name string cmdStubs func(*run.CommandStubber) remotes context.Remotes - expectedTrackingRef git.TrackingRef + expectedTrackingRef trackingRef expectedFound bool }{ { @@ -1636,7 +1636,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, - expectedTrackingRef: git.TrackingRef{}, + expectedTrackingRef: trackingRef{}, expectedFound: false, }, { @@ -1655,7 +1655,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, - expectedTrackingRef: git.TrackingRef{}, + expectedTrackingRef: trackingRef{}, expectedFound: false, }, { @@ -1678,9 +1678,9 @@ func Test_tryDetermineTrackingRef(t *testing.T) { Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, - expectedTrackingRef: git.TrackingRef{ - RemoteName: "origin", - BranchName: "feature", + expectedTrackingRef: trackingRef{ + remoteName: "origin", + branchName: "feature", }, expectedFound: true, }, @@ -1702,7 +1702,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, - expectedTrackingRef: git.TrackingRef{}, + expectedTrackingRef: trackingRef{}, expectedFound: false, }, } From c3b41e87b89aad1f8ea3fcbe4c93c08c3a50b3f9 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 6 Jan 2025 17:00:16 +0100 Subject: [PATCH 0101/2338] Panic mustParseTrackingRef if format is incorrect --- pkg/cmd/pr/create/create.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 46a4e284e2c..db160b419e3 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -530,9 +530,16 @@ func (r trackingRef) String() string { func mustParseTrackingRef(text string) trackingRef { parts := strings.SplitN(string(text), "/", 4) + // The only place this is called is tryDetermineTrackingRef, where we are reconstructing + // the same tracking ref we passed in. If it doesn't match the expected format, this is a + // programmer error we want to know about, so it's ok to panic. if len(parts) != 4 { - panic(fmt.Errorf("invalid tracking ref: %s", text)) + panic(fmt.Errorf("tracking ref should have four parts: %s", text)) } + if parts[0] != "refs" || parts[1] != "remotes" { + panic(fmt.Errorf("tracking ref should start with refs/remotes/: %s", text)) + } + return trackingRef{ remoteName: parts[2], branchName: parts[3], From 9051da39fc559ee73ffb1d870863c9100fd5c9f6 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 6 Jan 2025 10:19:47 -0700 Subject: [PATCH 0102/2338] provide additional logging and fallback Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 2519aa9e48f..51b67d33d9a 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -26,11 +26,13 @@ const ( // Allow injecting backoff interval in tests. var getAttestationRetryInterval = time.Millisecond * 200 +// githubApiClient makes REST calls to the GitHub API type githubApiClient interface { REST(hostname, method, p string, body io.Reader, data interface{}) error RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) } +// httpClient makes HTTP calls to all non-GitHub API endpoints type httpClient interface { Get(url string) (*http.Response, error) } @@ -180,7 +182,8 @@ func (c *LiveClient) fetchBundlesByURL(attestations []*Attestation) ([]*Attestat func (c *LiveClient) fetchBundleByURL(a *Attestation) (*bundle.Bundle, error) { if a.BundleURL == "" { - return nil, fmt.Errorf("bundle URL is empty") + c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") + return a.Bundle, nil } c.logger.VerbosePrintf("Fetching attestation bundle\n\n") From 311f2b2e236dc0f5a60026e1a5e920e8b063799e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 6 Jan 2025 10:39:40 -0700 Subject: [PATCH 0103/2338] return fetch attestations err directly Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 51b67d33d9a..d681be44617 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -69,7 +69,7 @@ func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Atte url := c.buildRepoAndDigestURL(repo, digest) attestations, err := c.getAttestations(url, repo, digest, limit) if err != nil { - return nil, fmt.Errorf("failed to fetch attestation by repo and digest: %w", err) + return nil, err } bundles, err := c.fetchBundlesByURL(attestations) From 070b67e5a42c68b7db066e6dd6118113eb16e7d3 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 6 Jan 2025 10:44:55 -0700 Subject: [PATCH 0104/2338] fetch bundles in parallel Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index d681be44617..1a29e304448 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -14,6 +14,7 @@ import ( "github.com/golang/snappy" v1 "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" "github.com/sigstore/sigstore-go/pkg/bundle" + "golang.org/x/sync/errgroup" "google.golang.org/protobuf/encoding/protojson" ) @@ -168,15 +169,24 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At func (c *LiveClient) fetchBundlesByURL(attestations []*Attestation) ([]*Attestation, error) { fetched := make([]*Attestation, len(attestations)) + g := errgroup.Group{} for i, a := range attestations { - b, err := c.fetchBundleByURL(a) - if err != nil { - return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) - } - fetched[i] = &Attestation{ - Bundle: b, - } + g.Go(func() error { + b, err := c.fetchBundleByURL(a) + if err != nil { + return fmt.Errorf("failed to fetch bundle with URL: %w", err) + } + fetched[i] = &Attestation{ + Bundle: b, + } + return nil + }) } + + if err := g.Wait(); err != nil { + return nil, err + } + return fetched, nil } From e03a36ea3c44e59bf0a1cf5e5a4fe082da13c734 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 6 Jan 2025 12:12:26 -0700 Subject: [PATCH 0105/2338] add tests for bundle url fetch and fallback Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 2 +- pkg/cmd/attestation/api/client_test.go | 53 ++++++++++++++++--- .../attestation/api/mock_httpClient_test.go | 8 +-- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 1a29e304448..d0ef3f21bf9 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -196,7 +196,7 @@ func (c *LiveClient) fetchBundleByURL(a *Attestation) (*bundle.Bundle, error) { return a.Bundle, nil } - c.logger.VerbosePrintf("Fetching attestation bundle\n\n") + c.logger.VerbosePrintf("Fetching attestation bundle with bundle URL\n\n") resp, err := c.httpClient.Get(a.BundleURL) if err != nil { diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 540ae1366d9..62b1c40cfda 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" "github.com/stretchr/testify/require" ) @@ -25,8 +26,8 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, }, - httpClient: mockHttpClient{ - OnGet: fetcher.OnGetSuccess, + httpClient: &mockHttpClient{ + OnGet: OnGetSuccess, }, logger: l, } @@ -36,8 +37,8 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccess, }, - httpClient: mockHttpClient{ - OnGet: fetcher.OnGetSuccess, + httpClient: &mockHttpClient{ + OnGet: OnGetSuccess, }, logger: l, } @@ -144,8 +145,8 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, }, - httpClient: mockHttpClient{ - OnGet: fetcher.OnGetSuccess, + httpClient: &mockHttpClient{ + OnGet: OnGetSuccess, }, logger: io.NewTestHandler(), } @@ -182,6 +183,42 @@ func TestGetByDigest_Error(t *testing.T) { require.Nil(t, attestations) } +func TestFetchBundleByURL(t *testing.T) { + t.Run("fetch by bundle URL successfully", func(t *testing.T) { + httpClient := mockHttpClient{ + OnGet: OnGetSuccess, + } + c := &LiveClient{ + httpClient: &httpClient, + logger: io.NewTestHandler(), + } + + attestation := makeTestAttestation() + bundle, err := c.fetchBundleByURL(&attestation) + require.NoError(t, err) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) + require.True(t, httpClient.called) + }) + + t.Run("fallback to bundle field when BundleURL field is empty", func(t *testing.T) { + httpClient := mockHttpClient{ + OnGet: OnGetSuccess, + } + c := &LiveClient{ + httpClient: &mockHttpClient{ + OnGet: OnGetSuccess, + }, + logger: io.NewTestHandler(), + } + + attestation := Attestation{Bundle: data.SigstoreBundle(t)} + bundle, err := c.fetchBundleByURL(&attestation) + require.NoError(t, err) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) + require.False(t, httpClient.called) + }) +} + func TestGetTrustDomain(t *testing.T) { fetcher := mockMetaGenerator{ TrustDomain: "foo", @@ -225,8 +262,8 @@ func TestGetAttestationsRetries(t *testing.T) { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.FlakyOnRESTSuccessWithNextPageHandler(), }, - httpClient: mockHttpClient{ - OnGet: fetcher.OnGetSuccess, + httpClient: &mockHttpClient{ + OnGet: OnGetSuccess, }, logger: io.NewTestHandler(), } diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 89584874fa1..365619ac1d3 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -10,14 +10,16 @@ import ( ) type mockHttpClient struct { - OnGet func(url string) (*http.Response, error) + called bool + OnGet func(url string) (*http.Response, error) } -func (m mockHttpClient) Get(url string) (*http.Response, error) { +func (m *mockHttpClient) Get(url string) (*http.Response, error) { + m.called = true return m.OnGet(url) } -func (m *mockDataGenerator) OnGetSuccess(url string) (*http.Response, error) { +func OnGetSuccess(url string) (*http.Response, error) { compressed := snappy.Encode(nil, data.SigstoreBundleRaw) return &http.Response{ StatusCode: 200, From 757966ca7cdd222b24c04eca2db8321db61a464f Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 6 Jan 2025 14:27:03 -0500 Subject: [PATCH 0106/2338] Bump cli/go-gh for indirect security vulnerability --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 98ee0280f81..f2f1ce81239 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( 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/cli/go-gh/v2 v2.11.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 github.com/cli/safeexec v1.0.1 diff --git a/go.sum b/go.sum index 49e104d077e..1b48f0c2744 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1 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= -github.com/cli/go-gh/v2 v2.11.1 h1:amAyfqMWQTBdue8iTmDUegGZK7c8kk6WCxD9l/wLtGI= -github.com/cli/go-gh/v2 v2.11.1/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= +github.com/cli/go-gh/v2 v2.11.2 h1:oad1+sESTPNTiTvh3I3t8UmxuovNDxhwLzeMHk45Q9w= +github.com/cli/go-gh/v2 v2.11.2/go.mod h1:vVFhi3TfjseIW26ED9itAR8gQK0aVThTm8sYrsZ5QTI= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk= github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs= From 0202ca8df5b645dfc92330a08bac63a111d8ace9 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 6 Jan 2025 12:58:09 -0700 Subject: [PATCH 0107/2338] add test case for bundle url fetch failure Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 71 +++++++++++-------- .../attestation/api/mock_httpClient_test.go | 7 ++ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 62b1c40cfda..4fb0f539a5c 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -184,39 +184,50 @@ func TestGetByDigest_Error(t *testing.T) { } func TestFetchBundleByURL(t *testing.T) { - t.Run("fetch by bundle URL successfully", func(t *testing.T) { - httpClient := mockHttpClient{ - OnGet: OnGetSuccess, - } - c := &LiveClient{ - httpClient: &httpClient, - logger: io.NewTestHandler(), - } + httpClient := mockHttpClient{ + OnGet: OnGetSuccess, + } + c := &LiveClient{ + httpClient: &httpClient, + logger: io.NewTestHandler(), + } - attestation := makeTestAttestation() - bundle, err := c.fetchBundleByURL(&attestation) - require.NoError(t, err) - require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.True(t, httpClient.called) - }) + attestation := makeTestAttestation() + bundle, err := c.fetchBundleByURL(&attestation) + require.NoError(t, err) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) + require.True(t, httpClient.called) +} +func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { + httpClient := mockHttpClient{ + OnGet: OnGetFail, + } + c := &LiveClient{ + httpClient: &httpClient, + logger: io.NewTestHandler(), + } - t.Run("fallback to bundle field when BundleURL field is empty", func(t *testing.T) { - httpClient := mockHttpClient{ - OnGet: OnGetSuccess, - } - c := &LiveClient{ - httpClient: &mockHttpClient{ - OnGet: OnGetSuccess, - }, - logger: io.NewTestHandler(), - } + attestation := makeTestAttestation() + bundle, err := c.fetchBundleByURL(&attestation) + require.Error(t, err) + require.Nil(t, bundle) + require.True(t, httpClient.called) +} - attestation := Attestation{Bundle: data.SigstoreBundle(t)} - bundle, err := c.fetchBundleByURL(&attestation) - require.NoError(t, err) - require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.False(t, httpClient.called) - }) +func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { + httpClient := mockHttpClient{ + OnGet: OnGetSuccess, + } + c := &LiveClient{ + httpClient: &httpClient, + logger: io.NewTestHandler(), + } + + attestation := Attestation{Bundle: data.SigstoreBundle(t)} + bundle, err := c.fetchBundleByURL(&attestation) + require.NoError(t, err) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) + require.False(t, httpClient.called) } func TestGetTrustDomain(t *testing.T) { diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 365619ac1d3..b89c101fed6 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "fmt" "io" "net/http" @@ -26,3 +27,9 @@ func OnGetSuccess(url string) (*http.Response, error) { Body: io.NopCloser(bytes.NewReader(compressed)), }, nil } + +func OnGetFail(url string) (*http.Response, error) { + return &http.Response{ + StatusCode: 500, + }, fmt.Errorf("failed to fetch with %s", url) +} From 69865117ab92abc2a1cb075b38f24302185de03e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 6 Jan 2025 13:14:02 -0700 Subject: [PATCH 0108/2338] add mutex for test field Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/mock_httpClient_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index b89c101fed6..2914d7887bb 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -5,18 +5,22 @@ import ( "fmt" "io" "net/http" + "sync" "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" "github.com/golang/snappy" ) type mockHttpClient struct { + mutex sync.RWMutex called bool OnGet func(url string) (*http.Response, error) } func (m *mockHttpClient) Get(url string) (*http.Response, error) { + m.mutex.Lock() m.called = true + m.mutex.Unlock() return m.OnGet(url) } From 0179381efdc45980596aae7a2147ce13440ec483 Mon Sep 17 00:00:00 2001 From: Frederick Zhang Date: Fri, 14 Jun 2024 14:43:38 +1000 Subject: [PATCH 0109/2338] Find PRs using @{push} When using a push.default = current central workflow [1], we should use @{push} instead to locate the remote branch. In fact, @{push} covers most cases in push.default = upstream too. The branch..merge is probably only needed when using RemoteURL and different remote / local branch names. [1] https://github.com/tpope/vim-fugitive/issues/1172#issuecomment-522301607 --- git/client.go | 3 ++ git/client_test.go | 37 +++++++++++++++++------- git/objects.go | 1 + pkg/cmd/pr/create/create_test.go | 49 ++++++++++++++++++++++++++++++++ pkg/cmd/pr/shared/finder.go | 4 ++- pkg/cmd/pr/shared/finder_test.go | 10 +++++++ pkg/cmd/pr/status/status.go | 4 ++- pkg/cmd/pr/status/status_test.go | 1 + 8 files changed, 96 insertions(+), 13 deletions(-) diff --git a/git/client.go b/git/client.go index 1a6d9ae7f08..99acb0ac93d 100644 --- a/git/client.go +++ b/git/client.go @@ -412,6 +412,9 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc cfg.MergeBase = parts[1] } } + if out, err = c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}"); err == nil { + cfg.Push = strings.TrimSuffix(string(out), "\n") + } return } diff --git a/git/client_test.go b/git/client_test.go index fff1397f397..c547d033574 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -727,28 +727,43 @@ func TestClientCommitBody(t *testing.T) { func TestClientReadBranchConfig(t *testing.T) { tests := []struct { name string - cmdExitStatus int - cmdStdout string - cmdStderr string - wantCmdArgs string + cmdExitStatus []int + cmdStdout []string + cmdStderr []string + wantCmdArgs []string wantBranchConfig BranchConfig }{ { name: "read branch config", - 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)$`, - wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk"}, + cmdExitStatus: []int{0, 0}, + cmdStdout: []string{"branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk", "origin/trunk"}, + cmdStderr: []string{"", ""}, + wantCmdArgs: []string{`path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`, `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`}, + wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk", Push: "origin/trunk"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + var cmds []*exec.Cmd + var cmdCtxs []commandCtx + for i := 0; i < len(tt.cmdExitStatus); i++ { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus[i], tt.cmdStdout[i], tt.cmdStderr[i]) + cmds = append(cmds, cmd) + cmdCtxs = append(cmdCtxs, cmdCtx) + } + i := -1 client := Client{ - GitPath: "path/to/git", - commandContext: cmdCtx, + GitPath: "path/to/git", + commandContext: func(ctx context.Context, name string, args ...string) *exec.Cmd { + i++ + cmdCtxs[i](ctx, name, args...) + return cmds[i] + }, } branchConfig := client.ReadBranchConfig(context.Background(), "trunk") - assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + for i := 0; i < len(tt.cmdExitStatus); i++ { + assert.Equal(t, tt.wantCmdArgs[i], strings.Join(cmds[i].Args[3:], " ")) + } assert.Equal(t, tt.wantBranchConfig, branchConfig) }) } diff --git a/git/objects.go b/git/objects.go index c096830425e..3b23d20b7f3 100644 --- a/git/objects.go +++ b/git/objects.go @@ -66,4 +66,5 @@ type BranchConfig struct { // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. MergeBase string MergeRef string + Push string } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 6c0ff11e226..2446b16c0af 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -354,6 +354,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", @@ -383,6 +384,9 @@ func Test_createRun(t *testing.T) { opts.HeadBranch = "feature" return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", }, { @@ -397,6 +401,9 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, expectedOutputs: []string{ "Would have created a Pull Request with:", `title: my title`, @@ -428,6 +435,9 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), @@ -488,6 +498,9 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, expectedOutputs: []string{ `Would have created a Pull Request with:`, `Title: my title`, @@ -525,6 +538,9 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), @@ -591,6 +607,9 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, expectedOut: heredoc.Doc(` Would have created a Pull Request with: Title: TITLE @@ -637,6 +656,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, @@ -700,6 +720,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, @@ -746,6 +767,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, @@ -795,6 +817,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "") @@ -853,6 +876,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD deadb00f refs/remotes/upstream/feature @@ -890,6 +914,7 @@ func Test_createRun(t *testing.T) { branch.feature.remote origin branch.feature.merge refs/heads/my-feat2 `)) // determineTrackingBranch + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/my-feat2") cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD deadbeef refs/remotes/origin/my-feat2 @@ -930,6 +955,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "d3476a1\u0000commit 0\u0000\u0000\n7a6ea13\u0000commit 1\u0000\u0000") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -970,6 +996,9 @@ func Test_createRun(t *testing.T) { opts.Milestone = "big one.oh" return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), @@ -1057,6 +1086,9 @@ func Test_createRun(t *testing.T) { opts.Finder = shared.NewMockFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")) return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, wantErr: "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123", }, { @@ -1073,6 +1105,7 @@ func Test_createRun(t *testing.T) { httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -1105,6 +1138,7 @@ func Test_createRun(t *testing.T) { mockRetrieveProjects(t, reg) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -1152,6 +1186,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature`, 0, "") cs.Register(`git rev-parse --show-toplevel`, 0, "") }, @@ -1211,6 +1246,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1259,6 +1295,7 @@ func Test_createRun(t *testing.T) { { name: "web long URL", cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, setup: func(opts *CreateOptions, t *testing.T) func() { @@ -1285,6 +1322,9 @@ func Test_createRun(t *testing.T) { } return func() {} }, + cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") + }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), @@ -1304,6 +1344,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, @@ -1379,6 +1420,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, @@ -1415,6 +1457,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, @@ -1465,6 +1508,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", @@ -1524,6 +1568,7 @@ func Test_createRun(t *testing.T) { deadbeef HEAD deadb00f refs/remotes/upstream/feature/feat2 deadbeef refs/remotes/origin/task1`)) // determineTrackingBranch + cs.Register(`git rev-parse --verify --quiet --abbrev-ref task1@\{push\}`, 1, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for monalisa:task1 into feature/feat2 in OWNER/REPO\n\n", @@ -1634,6 +1679,7 @@ 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 rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, expectedTrackingRef: trackingRef{}, @@ -1643,6 +1689,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { name: "no match", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature") }, remotes: context.Remotes{ @@ -1662,6 +1709,7 @@ 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 rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/feature") 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 @@ -1691,6 +1739,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { branch.feature.remote origin branch.feature.merge refs/heads/great-feat `)) + cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/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 diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index e4a70eb2230..23aee371c36 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -261,7 +261,9 @@ func (f *finder) parseCurrentBranch() (string, int, error) { } if gitRemoteRepo != nil { - if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { + if branchConfig.Push != "" { + prHeadRef = strings.TrimPrefix(branchConfig.Push, branchConfig.RemoteName+"/") + } else if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } // prepend `OWNER:` if this branch is pushed to a fork diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 258ef01e829..88ee170a01c 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -232,8 +232,17 @@ func TestFind(t *testing.T) { return "blueberries", nil }, branchConfig: func(branch string) (c git.BranchConfig) { + c.MergeRef = "refs/heads/blueberries" + c.RemoteName = "origin" + c.Push = "origin/blueberries" return }, + remotesFn: func() (context.Remotes, error) { + return context.Remotes{{ + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.New("OWNER", "REPO"), + }}, nil + }, }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -318,6 +327,7 @@ func TestFind(t *testing.T) { branchConfig: func(branch string) (c git.BranchConfig) { c.MergeRef = "refs/heads/blue-upstream-berries" c.RemoteName = "origin" + c.Push = "origin/blue-upstream-berries" return }, remotesFn: func() (context.Remotes, error) { diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 27666461d4c..ebba3313c2c 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -209,7 +209,9 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface } if branchOwner != "" { - if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { + if branchConfig.Push != "" { + selector = strings.TrimPrefix(branchConfig.Push, branchConfig.RemoteName+"/") + } else if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } // prepend `OWNER:` if this branch is pushed to a fork diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index e8e4875592d..9fbe484c544 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -383,6 +383,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { branch.Frederick888/main.remote git@github.com:Frederick888/playground.git branch.Frederick888/main.merge refs/heads/main `)) + rs.Register(`git rev-parse --verify --quiet --abbrev-ref Frederick888/main@\{push\}`, 1, "") repo := ghrepo.NewWithHost("octocat", "playground", "github.com") rem := context.Remotes{ From 7fc35fd47d9482068a5c7666c5c67d4e5e001fce Mon Sep 17 00:00:00 2001 From: Frederick Zhang Date: Fri, 14 Jun 2024 15:10:53 +1000 Subject: [PATCH 0110/2338] Only find PRs w/ branch..merge if push.default = upstream/tracking When push.default is not 'upstream' / 'tracking' (or 'nothing'), we can expect local and remote branch names to be the same and solely rely on @{push} or RemoteURL. This fixes the wrong error message 'no pull requests found for branch ""' when the local branch is not pushed in the push.default = simple / current and upstream = setup. --- pkg/cmd/pr/shared/finder.go | 9 ++++-- pkg/cmd/pr/shared/finder_test.go | 51 +++++++++++++++++++++++++++++++- pkg/cmd/pr/status/status.go | 3 +- pkg/cmd/pr/status/status_test.go | 34 ++++++++++++++++++++- 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 23aee371c36..6e36b4dd686 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -38,6 +38,7 @@ type finder struct { remotesFn func() (remotes.Remotes, error) httpClient func() (*http.Client, error) branchConfig func(string) git.BranchConfig + pushDefault func() (string, error) progress progressIndicator repo ghrepo.Interface @@ -57,7 +58,10 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { branchFn: factory.Branch, remotesFn: factory.Remotes, httpClient: factory.HttpClient, - progress: factory.IOStreams, + pushDefault: func() (string, error) { + return factory.GitClient.Config(context.Background(), "push.default") + }, + progress: factory.IOStreams, branchConfig: func(s string) git.BranchConfig { return factory.GitClient.ReadBranchConfig(context.Background(), s) }, @@ -263,7 +267,8 @@ func (f *finder) parseCurrentBranch() (string, int, error) { if gitRemoteRepo != nil { if branchConfig.Push != "" { prHeadRef = strings.TrimPrefix(branchConfig.Push, branchConfig.RemoteName+"/") - } else if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { + } else if pushDefault, _ := f.pushDefault(); (pushDefault == "upstream" || pushDefault == "tracking") && + strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } // prepend `OWNER:` if this branch is pushed to a fork diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 88ee170a01c..2c34833c827 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -18,6 +18,7 @@ func TestFind(t *testing.T) { baseRepoFn func() (ghrepo.Interface, error) branchFn func() (string, error) branchConfig func(string) git.BranchConfig + pushDefault func() (string, error) remotesFn func() (context.Remotes, error) selector string fields []string @@ -330,6 +331,7 @@ func TestFind(t *testing.T) { c.Push = "origin/blue-upstream-berries" return }, + pushDefault: func() (string, error) { return "upstream", nil }, remotesFn: func() (context.Remotes, error) { return context.Remotes{{ Remote: &git.Remote{Name: "origin"}, @@ -373,7 +375,52 @@ func TestFind(t *testing.T) { c.RemoteURL = u return }, - remotesFn: nil, + pushDefault: func() (string, error) { return "upstream", nil }, + remotesFn: nil, + }, + httpStub: func(r *httpmock.Registry) { + r.Register( + httpmock.GraphQL(`query PullRequestForBranch\b`), + httpmock.StringResponse(`{"data":{"repository":{ + "pullRequests":{"nodes":[ + { + "number": 13, + "state": "OPEN", + "baseRefName": "main", + "headRefName": "blue-upstream-berries", + "isCrossRepository": true, + "headRepositoryOwner": {"login":"UPSTREAMOWNER"} + } + ]} + }}}`)) + }, + wantPR: 13, + wantRepo: "https://github.com/OWNER/REPO", + }, + { + name: "current branch with tracking (deprecated synonym of upstream) configuration", + args: args{ + selector: "", + fields: []string{"id", "number"}, + baseRepoFn: func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("OWNER/REPO") + }, + branchFn: func() (string, error) { + return "blueberries", nil + }, + branchConfig: func(branch string) (c git.BranchConfig) { + c.MergeRef = "refs/heads/blue-upstream-berries" + c.RemoteName = "origin" + c.Push = "origin/blue-upstream-berries" + return + }, + pushDefault: func() (string, error) { return "tracking", nil }, + remotesFn: func() (context.Remotes, error) { + return context.Remotes{{ + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.New("UPSTREAMOWNER", "REPO"), + }}, nil + }, }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -407,6 +454,7 @@ func TestFind(t *testing.T) { }, branchConfig: func(branch string) (c git.BranchConfig) { c.RemoteName = "origin" + c.Push = "origin/blueberries" return }, remotesFn: func() (context.Remotes, error) { @@ -539,6 +587,7 @@ func TestFind(t *testing.T) { baseRepoFn: tt.args.baseRepoFn, branchFn: tt.args.branchFn, branchConfig: tt.args.branchConfig, + pushDefault: tt.args.pushDefault, remotesFn: tt.args.remotesFn, } diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index ebba3313c2c..1b6da842714 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -211,7 +211,8 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface if branchOwner != "" { if branchConfig.Push != "" { selector = strings.TrimPrefix(branchConfig.Push, branchConfig.RemoteName+"/") - } else if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { + } else if pushDefault, _ := gitClient.Config(context.Background(), "push.default"); (pushDefault == "upstream" || pushDefault == "tracking") && + strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } // prepend `OWNER:` if this branch is pushed to a fork diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 9fbe484c544..d09abf5b965 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -375,7 +375,7 @@ Requesting a code review from you } } -func Test_prSelectorForCurrentBranch(t *testing.T) { +func Test_prSelectorForCurrentBranchPushDefaultUpstream(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) @@ -384,6 +384,38 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { branch.Frederick888/main.merge refs/heads/main `)) rs.Register(`git rev-parse --verify --quiet --abbrev-ref Frederick888/main@\{push\}`, 1, "") + rs.Register(`git config push\.default`, 0, "upstream") + + repo := ghrepo.NewWithHost("octocat", "playground", "github.com") + rem := context.Remotes{ + &context.Remote{ + Remote: &git.Remote{Name: "origin"}, + Repo: repo, + }, + } + 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) + } +} + +func Test_prSelectorForCurrentBranchPushDefaultTracking(t *testing.T) { + rs, cleanup := run.Stub() + defer cleanup(t) + + 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 + `)) + rs.Register(`git rev-parse --verify --quiet --abbrev-ref Frederick888/main@\{push\}`, 1, "") + rs.Register(`git config push\.default`, 0, "tracking") repo := ghrepo.NewWithHost("octocat", "playground", "github.com") rem := context.Remotes{ From 4254818dbdfa59b4f51e524687bd6cd49e7b61c2 Mon Sep 17 00:00:00 2001 From: Frederick Zhang Date: Fri, 14 Jun 2024 19:00:31 +1000 Subject: [PATCH 0111/2338] Find push remote using branch..pushRemote and remote.pushDefault When using a push.default = current triangular workflow, apart from using @{push} to determine the remote branch name, we should also follow the 1. branch..pushRemote 2. remote.pushDefault 3. branch..remote ...list to determine which remote Git pushes to. --- git/client.go | 31 ++++-- git/client_test.go | 157 ++++++++++++++++++++++++++++--- git/objects.go | 8 +- pkg/cmd/pr/create/create_test.go | 35 ++++++- pkg/cmd/pr/shared/finder.go | 9 +- pkg/cmd/pr/shared/finder_test.go | 4 + pkg/cmd/pr/status/status.go | 6 +- pkg/cmd/pr/status/status_test.go | 2 + 8 files changed, 215 insertions(+), 37 deletions(-) diff --git a/git/client.go b/git/client.go index 99acb0ac93d..d82fd7b7838 100644 --- a/git/client.go +++ b/git/client.go @@ -379,7 +379,7 @@ 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) { prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch)) - args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|%s)$", prefix, MergeBaseConfig)} + args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|pushremote|%s)$", prefix, MergeBaseConfig)} cmd, err := c.Command(ctx, args...) if err != nil { return @@ -397,21 +397,22 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc keys := strings.Split(parts[0], ".") switch keys[len(keys)-1] { case "remote": - if strings.Contains(parts[1], ":") { - u, err := ParseURL(parts[1]) - if err != nil { - continue - } - cfg.RemoteURL = u - } else if !isFilesystemPath(parts[1]) { - cfg.RemoteName = parts[1] - } + parseRemoteURLOrName(parts[1], &cfg.RemoteURL, &cfg.RemoteName) + case "pushremote": + parseRemoteURLOrName(parts[1], &cfg.PushRemoteURL, &cfg.PushRemoteName) case "merge": cfg.MergeRef = parts[1] case MergeBaseConfig: cfg.MergeBase = parts[1] } } + if cfg.PushRemoteURL == nil && cfg.PushRemoteName == "" { + if conf, err := c.Config(ctx, "remote.pushDefault"); err == nil && conf != "" { + parseRemoteURLOrName(conf, &cfg.PushRemoteURL, &cfg.PushRemoteName) + } else { + cfg.PushRemoteName = cfg.RemoteName + } + } if out, err = c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}"); err == nil { cfg.Push = strings.TrimSuffix(string(out), "\n") } @@ -776,6 +777,16 @@ func parseRemotes(remotesStr []string) RemoteSet { return remotes } +func parseRemoteURLOrName(value string, remoteURL **url.URL, remoteName *string) { + if strings.Contains(value, ":") { + if u, err := ParseURL(value); err == nil { + *remoteURL = u + } + } else if !isFilesystemPath(value) { + *remoteName = value + } +} + func populateResolvedRemotes(remotes RemoteSet, resolved []string) { for _, l := range resolved { parts := strings.SplitN(l, " ", 2) diff --git a/git/client_test.go b/git/client_test.go index c547d033574..3136363c15c 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -725,31 +725,160 @@ func TestClientCommitBody(t *testing.T) { } func TestClientReadBranchConfig(t *testing.T) { + type cmdTest struct { + exitStatus int + stdOut string + stdErr string + wantArgs string + } tests := []struct { name string - cmdExitStatus []int - cmdStdout []string - cmdStderr []string - wantCmdArgs []string + cmds []cmdTest wantBranchConfig BranchConfig }{ { - name: "read branch config", - cmdExitStatus: []int{0, 0}, - cmdStdout: []string{"branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk", "origin/trunk"}, - cmdStderr: []string{"", ""}, - wantCmdArgs: []string{`path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`, `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`}, - wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk", Push: "origin/trunk"}, + name: "read branch config, central", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + wantArgs: `path/to/git config remote.pushDefault`, + }, + { + stdOut: "origin/trunk", + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk", PushRemoteName: "origin", Push: "origin/trunk"}, + }, + { + name: "read branch config, central, push.default = upstream", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk-remote", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + wantArgs: `path/to/git config remote.pushDefault`, + }, + { + stdOut: "origin/trunk-remote", + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk-remote", PushRemoteName: "origin", Push: "origin/trunk-remote"}, + }, + { + name: "read branch config, central, push.default = current", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/main", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + wantArgs: `path/to/git config remote.pushDefault`, + }, + { + stdOut: "origin/trunk", + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/main", PushRemoteName: "origin", Push: "origin/trunk"}, + }, + { + name: "read branch config, central, push.default = current, target branch not pushed, no existing remote branch", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote .\nbranch.trunk.merge refs/heads/trunk-middle", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + stdOut: "origin", + wantArgs: `path/to/git config remote.pushDefault`, + }, + { + exitStatus: 1, + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{MergeRef: "refs/heads/trunk-middle", PushRemoteName: "origin"}, + }, + { + name: "read branch config, triangular, push.default = current, has existing remote branch, branch.trunk.pushremote effective", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main\nbranch.trunk.pushremote origin", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + stdOut: "origin/trunk", + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin", Push: "origin/trunk"}, + }, + { + name: "read branch config, triangular, push.default = current, has existing remote branch, remote.pushDefault effective", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + stdOut: "origin", + wantArgs: `path/to/git config remote.pushDefault`, + }, + { + stdOut: "origin/trunk", + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin", Push: "origin/trunk"}, + }, + { + name: "read branch config, triangular, push.default = current, no existing remote branch, branch.trunk.pushremote effective", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main\nbranch.trunk.pushremote origin", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + exitStatus: 1, + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin"}, + }, + { + name: "read branch config, triangular, push.default = current, no existing remote branch, remote.pushDefault effective", + cmds: []cmdTest{ + { + stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main", + wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`, + }, + { + stdOut: "origin", + wantArgs: `path/to/git config remote.pushDefault`, + }, + { + exitStatus: 1, + wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`, + }, + }, + wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var cmds []*exec.Cmd var cmdCtxs []commandCtx - for i := 0; i < len(tt.cmdExitStatus); i++ { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus[i], tt.cmdStdout[i], tt.cmdStderr[i]) + for _, c := range tt.cmds { + cmd, cmdCtx := createCommandContext(t, c.exitStatus, c.stdOut, c.stdErr) cmds = append(cmds, cmd) cmdCtxs = append(cmdCtxs, cmdCtx) + } i := -1 client := Client{ @@ -761,8 +890,8 @@ func TestClientReadBranchConfig(t *testing.T) { }, } branchConfig := client.ReadBranchConfig(context.Background(), "trunk") - for i := 0; i < len(tt.cmdExitStatus); i++ { - assert.Equal(t, tt.wantCmdArgs[i], strings.Join(cmds[i].Args[3:], " ")) + for i := 0; i < len(tt.cmds); i++ { + assert.Equal(t, tt.cmds[i].wantArgs, strings.Join(cmds[i].Args[3:], " ")) } assert.Equal(t, tt.wantBranchConfig, branchConfig) }) diff --git a/git/objects.go b/git/objects.go index 3b23d20b7f3..eff209be4e0 100644 --- a/git/objects.go +++ b/git/objects.go @@ -64,7 +64,9 @@ type BranchConfig struct { RemoteName string RemoteURL *url.URL // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. - MergeBase string - MergeRef string - Push string + MergeBase string + MergeRef string + PushRemoteURL *url.URL + PushRemoteName string + Push string } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 2446b16c0af..073dfb87917 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -354,6 +354,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, @@ -385,6 +386,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", @@ -402,6 +404,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, expectedOutputs: []string{ @@ -436,6 +439,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { @@ -499,6 +503,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, expectedOutputs: []string{ @@ -539,6 +544,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { @@ -608,6 +614,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, expectedOut: heredoc.Doc(` @@ -656,6 +663,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -720,6 +728,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -767,6 +776,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -817,6 +827,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register("git remote rename origin upstream", 0, "") @@ -876,6 +887,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD @@ -914,6 +926,7 @@ func Test_createRun(t *testing.T) { branch.feature.remote origin branch.feature.merge refs/heads/my-feat2 `)) // determineTrackingBranch + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/my-feat2") cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD @@ -955,6 +968,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "d3476a1\u0000commit 0\u0000\u0000\n7a6ea13\u0000commit 1\u0000\u0000") }, @@ -997,6 +1011,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { @@ -1087,6 +1102,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, wantErr: "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123", @@ -1105,6 +1121,7 @@ func Test_createRun(t *testing.T) { httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") @@ -1138,6 +1155,7 @@ func Test_createRun(t *testing.T) { mockRetrieveProjects(t, reg) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") @@ -1186,6 +1204,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature`, 0, "") cs.Register(`git rev-parse --show-toplevel`, 0, "") @@ -1246,6 +1265,7 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, @@ -1295,6 +1315,7 @@ func Test_createRun(t *testing.T) { { name: "web long URL", cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, @@ -1323,6 +1344,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { @@ -1344,6 +1366,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", @@ -1420,6 +1443,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", @@ -1457,6 +1481,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", @@ -1508,6 +1533,7 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "") }, @@ -1560,7 +1586,7 @@ func Test_createRun(t *testing.T) { }, customBranchConfig: true, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config --get-regexp \^branch\\\.task1\\\.\(remote\|merge\|gh-merge-base\)\$`, 0, heredoc.Doc(` + cs.Register(`git config --get-regexp \^branch\\\.task1\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, heredoc.Doc(` branch.task1.remote origin branch.task1.merge refs/heads/task1 branch.task1.gh-merge-base feature/feat2`)) // ReadBranchConfig @@ -1568,6 +1594,7 @@ func Test_createRun(t *testing.T) { deadbeef HEAD deadb00f refs/remotes/upstream/feature/feat2 deadbeef refs/remotes/origin/task1`)) // determineTrackingBranch + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref task1@\{push\}`, 1, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", @@ -1595,7 +1622,7 @@ func Test_createRun(t *testing.T) { defer cmdTeardown(t) cs.Register(`git status --porcelain`, 0, "") if !tt.customBranchConfig { - cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|gh-merge-base\)\$`, 0, "") + cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, "") } if tt.cmdStubs != nil { @@ -1679,6 +1706,7 @@ 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 config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, @@ -1689,6 +1717,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { name: "no match", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature") }, @@ -1709,6 +1738,7 @@ 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 config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/feature") cs.Register(`git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature$`, 0, heredoc.Doc(` deadbeef HEAD @@ -1739,6 +1769,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { branch.feature.remote origin branch.feature.merge refs/heads/great-feat `)) + cs.Register(`git config remote.pushDefault`, 1, "") cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/great-feat") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(` deadbeef HEAD diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 6e36b4dd686..f2c8f9f92be 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -251,22 +251,21 @@ func (f *finder) parseCurrentBranch() (string, int, error) { } var gitRemoteRepo ghrepo.Interface - if branchConfig.RemoteURL != nil { + if branchConfig.PushRemoteURL != nil { // the branch merges from a remote specified by URL if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil { gitRemoteRepo = r } - } else if branchConfig.RemoteName != "" { - // the branch merges from a remote specified by name + } else if branchConfig.PushRemoteName != "" { rem, _ := f.remotesFn() - if r, err := rem.FindByName(branchConfig.RemoteName); err == nil { + if r, err := rem.FindByName(branchConfig.PushRemoteName); err == nil { gitRemoteRepo = r } } if gitRemoteRepo != nil { if branchConfig.Push != "" { - prHeadRef = strings.TrimPrefix(branchConfig.Push, branchConfig.RemoteName+"/") + prHeadRef = strings.TrimPrefix(branchConfig.Push, branchConfig.PushRemoteName+"/") } else if pushDefault, _ := f.pushDefault(); (pushDefault == "upstream" || pushDefault == "tracking") && strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 2c34833c827..7c9f1ab5766 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -328,6 +328,7 @@ func TestFind(t *testing.T) { branchConfig: func(branch string) (c git.BranchConfig) { c.MergeRef = "refs/heads/blue-upstream-berries" c.RemoteName = "origin" + c.PushRemoteName = "origin" c.Push = "origin/blue-upstream-berries" return }, @@ -373,6 +374,7 @@ func TestFind(t *testing.T) { u, _ := url.Parse("https://github.com/UPSTREAMOWNER/REPO") c.MergeRef = "refs/heads/blue-upstream-berries" c.RemoteURL = u + c.PushRemoteURL = u return }, pushDefault: func() (string, error) { return "upstream", nil }, @@ -411,6 +413,7 @@ func TestFind(t *testing.T) { branchConfig: func(branch string) (c git.BranchConfig) { c.MergeRef = "refs/heads/blue-upstream-berries" c.RemoteName = "origin" + c.PushRemoteName = "origin" c.Push = "origin/blue-upstream-berries" return }, @@ -454,6 +457,7 @@ func TestFind(t *testing.T) { }, branchConfig: func(branch string) (c git.BranchConfig) { c.RemoteName = "origin" + c.PushRemoteName = "origin" c.Push = "origin/blueberries" return }, diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 1b6da842714..33fc096c8e8 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -201,16 +201,16 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil { branchOwner = r.RepoOwner() } - } else if branchConfig.RemoteName != "" { + } else if branchConfig.PushRemoteName != "" { // the branch merges from a remote specified by name - if r, err := rem.FindByName(branchConfig.RemoteName); err == nil { + if r, err := rem.FindByName(branchConfig.PushRemoteName); err == nil { branchOwner = r.RepoOwner() } } if branchOwner != "" { if branchConfig.Push != "" { - selector = strings.TrimPrefix(branchConfig.Push, branchConfig.RemoteName+"/") + selector = strings.TrimPrefix(branchConfig.Push, branchConfig.PushRemoteName+"/") } else if pushDefault, _ := gitClient.Config(context.Background(), "push.default"); (pushDefault == "upstream" || pushDefault == "tracking") && strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index d09abf5b965..e60764c4f2c 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -383,6 +383,7 @@ func Test_prSelectorForCurrentBranchPushDefaultUpstream(t *testing.T) { branch.Frederick888/main.remote git@github.com:Frederick888/playground.git branch.Frederick888/main.merge refs/heads/main `)) + rs.Register(`git config remote.pushDefault`, 1, "") rs.Register(`git rev-parse --verify --quiet --abbrev-ref Frederick888/main@\{push\}`, 1, "") rs.Register(`git config push\.default`, 0, "upstream") @@ -414,6 +415,7 @@ func Test_prSelectorForCurrentBranchPushDefaultTracking(t *testing.T) { branch.Frederick888/main.remote git@github.com:Frederick888/playground.git branch.Frederick888/main.merge refs/heads/main `)) + rs.Register(`git config remote.pushDefault`, 1, "") rs.Register(`git rev-parse --verify --quiet --abbrev-ref Frederick888/main@\{push\}`, 1, "") rs.Register(`git config push\.default`, 0, "tracking") From 2d1e4d625bc2a31c61c70452388188b327032b3c Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 16 Dec 2024 12:02:09 -0500 Subject: [PATCH 0112/2338] Add base gh pr view acceptance tests for changes These are acceptance tests based on @williammartin work within various issues associated with #9208. These need to be enhanced to account for `gh pr status`, however that should be quicker given these pass based on the updated branch and upstream fix for cross-repo same org changes. --- .../pr-view-respects-branch-pushremote.txtar | 42 +++++++++++++++++++ .../pr-view-respects-push-destination.txtar | 33 +++++++++++++++ .../pr-view-respects-remote-pushdefault.txtar | 42 +++++++++++++++++++ .../pr-view-respects-simple-pushdefault.txtar | 30 +++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar create mode 100644 acceptance/testdata/pr/pr-view-respects-push-destination.txtar create mode 100644 acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar create mode 100644 acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar diff --git a/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar new file mode 100644 index 00000000000..3e79e755d17 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar @@ -0,0 +1,42 @@ +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository as opposed to private organization fork +exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${ORG}/${FORK} +sleep 5 +exec gh repo view ${ORG}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${ORG}/${FORK} +cd ${FORK} + +# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork +exec git checkout -b feature-branch upstream/main +exec git config branch.feature-branch.pushRemote origin +exec git config --list +exec git commit --allow-empty -m 'Empty Commit' +exec git push + +# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks +exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' + +# View the PR +env GH_DEBUG=api +exec gh pr view +stdout 'Feature Title' diff --git a/acceptance/testdata/pr/pr-view-respects-push-destination.txtar b/acceptance/testdata/pr/pr-view-respects-push-destination.txtar new file mode 100644 index 00000000000..6772d6cfddf --- /dev/null +++ b/acceptance/testdata/pr/pr-view-respects-push-destination.txtar @@ -0,0 +1,33 @@ +# Setup environment variables used for testscript +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} + +# Clone the repo +exec gh repo clone ${ORG}/${REPO} +cd ${REPO} + +# Configure default push behavior so local and remote branches will be the same +exec git config push.default current + +# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name +exec git checkout -b test-feature +exec git branch --set-upstream-to origin/main +exec git rev-parse --abbrev-ref test-feature@{upstream} +stdout origin/main + +# Create the PR +exec git commit --allow-empty -m 'Empty Commit' +exec git push +exec gh pr create -B main -H test-feature --title 'Feature Title' --body 'Feature Body' + +# View the PR +exec gh pr view +stdout 'Feature Title' diff --git a/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar new file mode 100644 index 00000000000..2a8a92826a4 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar @@ -0,0 +1,42 @@ +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository as opposed to private organization fork +exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${ORG}/${FORK} +sleep 5 +exec gh repo view ${ORG}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${ORG}/${FORK} +cd ${FORK} + +# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork +exec git checkout -b feature-branch upstream/main +exec git config remote.pushDefault origin +exec git config --list +exec git commit --allow-empty -m 'Empty Commit' +exec git push + +# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks +exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' + +# View the PR +env GH_DEBUG=api +exec gh pr view +stdout 'Feature Title' diff --git a/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar new file mode 100644 index 00000000000..55da849ac4a --- /dev/null +++ b/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar @@ -0,0 +1,30 @@ +# Setup environment variables used for testscript +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} + +# Clone the repo +exec gh repo clone ${ORG}/${REPO} +cd ${REPO} + +# Configure default push behavior so local and remote branches have to be the same +exec git config push.default simple + +# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name +exec git checkout -b test-feature origin/main + +# Create the PR +exec git commit --allow-empty -m 'Empty Commit' +exec git push origin test-feature +exec gh pr create -H test-feature --title 'Feature Title' --body 'Feature Body' + +# View the PR +exec gh pr view +stdout 'Feature Title' From eb16a75ab1a02b7a7f309c8de4c03528fe83e1eb Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 16 Dec 2024 14:24:59 -0500 Subject: [PATCH 0113/2338] Expand with gh pr status --- .../pr/pr-view-respects-branch-pushremote.txtar | 7 +++++-- .../pr/pr-view-respects-push-destination.txtar | 11 ++++++++--- .../pr/pr-view-respects-remote-pushdefault.txtar | 7 +++++-- .../pr/pr-view-respects-simple-pushdefault.txtar | 11 ++++++++--- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar index 3e79e755d17..ef80cd8babf 100644 --- a/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar +++ b/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar @@ -29,7 +29,6 @@ cd ${FORK} # Prepare a branch where changes are pulled from the upstream default branch but pushed to fork exec git checkout -b feature-branch upstream/main exec git config branch.feature-branch.pushRemote origin -exec git config --list exec git commit --allow-empty -m 'Empty Commit' exec git push @@ -37,6 +36,10 @@ exec git push exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' # View the PR -env GH_DEBUG=api exec gh pr view stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-respects-push-destination.txtar b/acceptance/testdata/pr/pr-view-respects-push-destination.txtar index 6772d6cfddf..ff9db4037c0 100644 --- a/acceptance/testdata/pr/pr-view-respects-push-destination.txtar +++ b/acceptance/testdata/pr/pr-view-respects-push-destination.txtar @@ -18,16 +18,21 @@ cd ${REPO} exec git config push.default current # Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b test-feature +exec git checkout -b feature-branch exec git branch --set-upstream-to origin/main -exec git rev-parse --abbrev-ref test-feature@{upstream} +exec git rev-parse --abbrev-ref feature-branch@{upstream} stdout origin/main # Create the PR exec git commit --allow-empty -m 'Empty Commit' exec git push -exec gh pr create -B main -H test-feature --title 'Feature Title' --body 'Feature Body' +exec gh pr create -B main -H feature-branch --title 'Feature Title' --body 'Feature Body' # View the PR exec gh pr view stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar index 2a8a92826a4..8bfac28376a 100644 --- a/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar +++ b/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar @@ -29,7 +29,6 @@ cd ${FORK} # Prepare a branch where changes are pulled from the upstream default branch but pushed to fork exec git checkout -b feature-branch upstream/main exec git config remote.pushDefault origin -exec git config --list exec git commit --allow-empty -m 'Empty Commit' exec git push @@ -37,6 +36,10 @@ exec git push exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' # View the PR -env GH_DEBUG=api exec gh pr view stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar index 55da849ac4a..114f401ecb6 100644 --- a/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar +++ b/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar @@ -18,13 +18,18 @@ cd ${REPO} exec git config push.default simple # Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b test-feature origin/main +exec git checkout -b feature-branch origin/main # Create the PR exec git commit --allow-empty -m 'Empty Commit' -exec git push origin test-feature -exec gh pr create -H test-feature --title 'Feature Title' --body 'Feature Body' +exec git push origin feature-branch +exec gh pr create -H feature-branch --title 'Feature Title' --body 'Feature Body' # View the PR exec gh pr view stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH From 8bb2879b87b9be2d104f6b3e6266d2058e117252 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 16 Dec 2024 14:26:13 -0500 Subject: [PATCH 0114/2338] Reflect coverage for view and status subcommands --- .../pr-view-respects-branch-pushremote.txtar | 45 ------------------- .../pr-view-respects-push-destination.txtar | 38 ---------------- .../pr-view-respects-remote-pushdefault.txtar | 45 ------------------- .../pr-view-respects-simple-pushdefault.txtar | 35 --------------- 4 files changed, 163 deletions(-) delete mode 100644 acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar delete mode 100644 acceptance/testdata/pr/pr-view-respects-push-destination.txtar delete mode 100644 acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar delete mode 100644 acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar diff --git a/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar deleted file mode 100644 index ef80cd8babf..00000000000 --- a/acceptance/testdata/pr/pr-view-respects-branch-pushremote.txtar +++ /dev/null @@ -1,45 +0,0 @@ -# Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} -env FORK=${REPO}-fork - -# Use gh as a credential helper -exec gh auth setup-git - -# Create a repository to act as upstream with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup of upstream -defer gh repo delete --yes ${ORG}/${REPO} -exec gh repo view ${ORG}/${REPO} --json id --jq '.id' -stdout2env REPO_ID - -# Create a user fork of repository as opposed to private organization fork -exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK} - -# Defer repo cleanup of fork -defer gh repo delete --yes ${ORG}/${FORK} -sleep 5 -exec gh repo view ${ORG}/${FORK} --json id --jq '.id' -stdout2env FORK_ID - -# Clone the repo -exec gh repo clone ${ORG}/${FORK} -cd ${FORK} - -# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork -exec git checkout -b feature-branch upstream/main -exec git config branch.feature-branch.pushRemote origin -exec git commit --allow-empty -m 'Empty Commit' -exec git push - -# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks -exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' - -# View the PR -exec gh pr view -stdout 'Feature Title' - -# Check the PR status -env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch] -exec gh pr status -stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-respects-push-destination.txtar b/acceptance/testdata/pr/pr-view-respects-push-destination.txtar deleted file mode 100644 index ff9db4037c0..00000000000 --- a/acceptance/testdata/pr/pr-view-respects-push-destination.txtar +++ /dev/null @@ -1,38 +0,0 @@ -# Setup environment variables used for testscript -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} - -# Clone the repo -exec gh repo clone ${ORG}/${REPO} -cd ${REPO} - -# Configure default push behavior so local and remote branches will be the same -exec git config push.default current - -# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b feature-branch -exec git branch --set-upstream-to origin/main -exec git rev-parse --abbrev-ref feature-branch@{upstream} -stdout origin/main - -# Create the PR -exec git commit --allow-empty -m 'Empty Commit' -exec git push -exec gh pr create -B main -H feature-branch --title 'Feature Title' --body 'Feature Body' - -# View the PR -exec gh pr view -stdout 'Feature Title' - -# Check the PR status -env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] -exec gh pr status -stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar deleted file mode 100644 index 8bfac28376a..00000000000 --- a/acceptance/testdata/pr/pr-view-respects-remote-pushdefault.txtar +++ /dev/null @@ -1,45 +0,0 @@ -# Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} -env FORK=${REPO}-fork - -# Use gh as a credential helper -exec gh auth setup-git - -# Create a repository to act as upstream with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup of upstream -defer gh repo delete --yes ${ORG}/${REPO} -exec gh repo view ${ORG}/${REPO} --json id --jq '.id' -stdout2env REPO_ID - -# Create a user fork of repository as opposed to private organization fork -exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK} - -# Defer repo cleanup of fork -defer gh repo delete --yes ${ORG}/${FORK} -sleep 5 -exec gh repo view ${ORG}/${FORK} --json id --jq '.id' -stdout2env FORK_ID - -# Clone the repo -exec gh repo clone ${ORG}/${FORK} -cd ${FORK} - -# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork -exec git checkout -b feature-branch upstream/main -exec git config remote.pushDefault origin -exec git commit --allow-empty -m 'Empty Commit' -exec git push - -# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks -exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' - -# View the PR -exec gh pr view -stdout 'Feature Title' - -# Check the PR status -env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch] -exec gh pr status -stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar deleted file mode 100644 index 114f401ecb6..00000000000 --- a/acceptance/testdata/pr/pr-view-respects-simple-pushdefault.txtar +++ /dev/null @@ -1,35 +0,0 @@ -# Setup environment variables used for testscript -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} - -# Clone the repo -exec gh repo clone ${ORG}/${REPO} -cd ${REPO} - -# Configure default push behavior so local and remote branches have to be the same -exec git config push.default simple - -# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b feature-branch origin/main - -# Create the PR -exec git commit --allow-empty -m 'Empty Commit' -exec git push origin feature-branch -exec gh pr create -H feature-branch --title 'Feature Title' --body 'Feature Body' - -# View the PR -exec gh pr view -stdout 'Feature Title' - -# Check the PR status -env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] -exec gh pr status -stdout $PR_STATUS_BRANCH From be250b3d332d42fe16bf2b420df2debfc2482a1c Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 16 Dec 2024 15:36:43 -0500 Subject: [PATCH 0115/2338] Add renamed acceptance tests --- ...ew-status-respects-branch-pushremote.txtar | 45 +++++++++++++++++++ ...iew-status-respects-push-destination.txtar | 38 ++++++++++++++++ ...w-status-respects-remote-pushdefault.txtar | 45 +++++++++++++++++++ ...w-status-respects-simple-pushdefault.txtar | 35 +++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar create mode 100644 acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar create mode 100644 acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar create mode 100644 acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar diff --git a/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar new file mode 100644 index 00000000000..ef80cd8babf --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar @@ -0,0 +1,45 @@ +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository as opposed to private organization fork +exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${ORG}/${FORK} +sleep 5 +exec gh repo view ${ORG}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${ORG}/${FORK} +cd ${FORK} + +# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork +exec git checkout -b feature-branch upstream/main +exec git config branch.feature-branch.pushRemote origin +exec git commit --allow-empty -m 'Empty Commit' +exec git push + +# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks +exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' + +# View the PR +exec gh pr view +stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar b/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar new file mode 100644 index 00000000000..ff9db4037c0 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar @@ -0,0 +1,38 @@ +# Setup environment variables used for testscript +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} + +# Clone the repo +exec gh repo clone ${ORG}/${REPO} +cd ${REPO} + +# Configure default push behavior so local and remote branches will be the same +exec git config push.default current + +# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name +exec git checkout -b feature-branch +exec git branch --set-upstream-to origin/main +exec git rev-parse --abbrev-ref feature-branch@{upstream} +stdout origin/main + +# Create the PR +exec git commit --allow-empty -m 'Empty Commit' +exec git push +exec gh pr create -B main -H feature-branch --title 'Feature Title' --body 'Feature Body' + +# View the PR +exec gh pr view +stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar new file mode 100644 index 00000000000..8bfac28376a --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar @@ -0,0 +1,45 @@ +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository as opposed to private organization fork +exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${ORG}/${FORK} +sleep 5 +exec gh repo view ${ORG}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${ORG}/${FORK} +cd ${FORK} + +# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork +exec git checkout -b feature-branch upstream/main +exec git config remote.pushDefault origin +exec git commit --allow-empty -m 'Empty Commit' +exec git push + +# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks +exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }' + +# View the PR +exec gh pr view +stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH diff --git a/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar new file mode 100644 index 00000000000..114f401ecb6 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar @@ -0,0 +1,35 @@ +# Setup environment variables used for testscript +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} + +# Clone the repo +exec gh repo clone ${ORG}/${REPO} +cd ${REPO} + +# Configure default push behavior so local and remote branches have to be the same +exec git config push.default simple + +# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name +exec git checkout -b feature-branch origin/main + +# Create the PR +exec git commit --allow-empty -m 'Empty Commit' +exec git push origin feature-branch +exec gh pr create -H feature-branch --title 'Feature Title' --body 'Feature Body' + +# View the PR +exec gh pr view +stdout 'Feature Title' + +# Check the PR status +env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] +exec gh pr status +stdout $PR_STATUS_BRANCH From 0006091d766ce3016ff3f2c84d3dc430bd2349bb Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 18 Dec 2024 12:26:43 +1100 Subject: [PATCH 0116/2338] Fix up intra-org fork test setup [1] https://github.com/cli/cli/commit/96ac8d6a2fb268a5825e155ae398e640d3e472fd --- pkg/cmd/pr/shared/finder_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 7c9f1ab5766..ffa0615a9d8 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -456,6 +456,7 @@ func TestFind(t *testing.T) { return "blueberries", nil }, branchConfig: func(branch string) (c git.BranchConfig) { + c.MergeRef = "refs/heads/main" c.RemoteName = "origin" c.PushRemoteName = "origin" c.Push = "origin/blueberries" @@ -465,6 +466,9 @@ func TestFind(t *testing.T) { return context.Remotes{{ Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.New("OWNER", "REPO-FORK"), + }, { + Remote: &git.Remote{Name: "upstream"}, + Repo: ghrepo.New("OWNER", "REPO"), }}, nil }, }, From 3b69ea6fbc603f631711c31a443e43744eaeb08c Mon Sep 17 00:00:00 2001 From: nobe4 Date: Tue, 7 Jan 2025 12:01:02 +0100 Subject: [PATCH 0117/2338] feat: make the extension upgrade fancier This left-pad the extension names during upgrade so they all align nicely. It makes easier to see what is happening. --- pkg/cmd/extension/manager.go | 11 ++++++++++- pkg/cmd/extension/manager_test.go | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 269d58ae055..49c448b7e45 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -469,9 +469,18 @@ func (m *Manager) Upgrade(name string, force bool) error { } func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error { + var longestExtName = 0 + for _, ext := range exts { + l := len(ext.Name()) + if len(ext.Name()) > longestExtName { + longestExtName = l + } + } + format := fmt.Sprintf("[%%%ds]: ", longestExtName) + var failed bool for _, f := range exts { - fmt.Fprintf(m.io.Out, "[%s]: ", f.Name()) + fmt.Fprintf(m.io.Out, format, f.Name()) currentVersion := displayExtensionVersion(f, f.CurrentVersion()) err := m.upgradeExtension(f, force) if err != nil { diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 025d6e12598..b83f171850e 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -256,7 +256,7 @@ func TestManager_UpgradeExtensions(t *testing.T) { ` [hello]: upgraded from old vers to new vers [local]: local extensions can not be upgraded - [two]: upgraded from old vers to new vers + [ two]: upgraded from old vers to new vers `, ), stdout.String()) assert.Equal(t, "", stderr.String()) @@ -293,7 +293,7 @@ func TestManager_UpgradeExtensions_DryRun(t *testing.T) { ` [hello]: would have upgraded from 0 to 1 [local]: local extensions can not be upgraded - [two]: would have upgraded from 2 to 3 + [ two]: would have upgraded from 2 to 3 `, ), stdout.String()) assert.Equal(t, "", stderr.String()) From 9ecd90c26ceb564e0dea8f492f9f42da952ab6c8 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 10:24:42 -0700 Subject: [PATCH 0118/2338] setup testing struct for test cases Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 89 ++++++++++++------- .../attestation/api/mock_httpClient_test.go | 26 +++--- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 4fb0f539a5c..0f91aff7ea1 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -21,15 +21,15 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { } l := io.NewTestHandler() + mockHTTPClient := &mockHttpClient{} + if hasNextPage { return &LiveClient{ githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, }, - httpClient: &mockHttpClient{ - OnGet: OnGetSuccess, - }, - logger: l, + httpClient: mockHTTPClient, + logger: l, } } @@ -37,10 +37,8 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccess, }, - httpClient: &mockHttpClient{ - OnGet: OnGetSuccess, - }, - logger: l, + httpClient: mockHTTPClient, + logger: l, } } @@ -145,10 +143,8 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, }, - httpClient: &mockHttpClient{ - OnGet: OnGetSuccess, - }, - logger: io.NewTestHandler(), + httpClient: &mockHttpClient{}, + logger: io.NewTestHandler(), } attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) @@ -183,12 +179,46 @@ func TestGetByDigest_Error(t *testing.T) { require.Nil(t, attestations) } -func TestFetchBundleByURL(t *testing.T) { - httpClient := mockHttpClient{ - OnGet: OnGetSuccess, +func TestFetchBundlesByURL(t *testing.T) { + mockHTTPClient := &mockHttpClient{} + client := LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), + } + + att1 := makeTestAttestation() + att2 := makeTestAttestation() + attestations := []*Attestation{&att1, &att2} + fetched, err := client.fetchBundlesByURL(attestations) + require.NoError(t, err) + require.Len(t, fetched, 2) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) + require.Equal(t, 2, mockHTTPClient.currNumCalls) +} + +func TestFetchBundlesByURL_Fail(t *testing.T) { + mockHTTPClient := &mockHttpClient{ + failAfterNumCalls: 2, + } + + c := &LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), } + + att1 := makeTestAttestation() + att2 := makeTestAttestation() + attestations := []*Attestation{&att1, &att2} + fetched, err := c.fetchBundlesByURL(attestations) + require.Error(t, err) + require.Nil(t, fetched) +} + +func TestFetchBundleByURL(t *testing.T) { + mockHTTPClient := &mockHttpClient{} + c := &LiveClient{ - httpClient: &httpClient, + httpClient: mockHTTPClient, logger: io.NewTestHandler(), } @@ -196,14 +226,16 @@ func TestFetchBundleByURL(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.True(t, httpClient.called) + require.Equal(t, 1, mockHTTPClient.currNumCalls) } + func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { - httpClient := mockHttpClient{ - OnGet: OnGetFail, + mockHTTPClient := &mockHttpClient{ + failAfterNumCalls: 1, } + c := &LiveClient{ - httpClient: &httpClient, + httpClient: mockHTTPClient, logger: io.NewTestHandler(), } @@ -211,15 +243,14 @@ func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.Error(t, err) require.Nil(t, bundle) - require.True(t, httpClient.called) + require.Equal(t, 1, mockHTTPClient.currNumCalls) } func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { - httpClient := mockHttpClient{ - OnGet: OnGetSuccess, - } + mockHTTPClient := &mockHttpClient{} + c := &LiveClient{ - httpClient: &httpClient, + httpClient: mockHTTPClient, logger: io.NewTestHandler(), } @@ -227,7 +258,7 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.False(t, httpClient.called) + require.Equal(t, 0, mockHTTPClient.currNumCalls) } func TestGetTrustDomain(t *testing.T) { @@ -273,10 +304,8 @@ func TestGetAttestationsRetries(t *testing.T) { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.FlakyOnRESTSuccessWithNextPageHandler(), }, - httpClient: &mockHttpClient{ - OnGet: OnGetSuccess, - }, - logger: io.NewTestHandler(), + httpClient: &mockHttpClient{}, + logger: io.NewTestHandler(), } attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 2914d7887bb..3218ed508fe 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -12,28 +12,26 @@ import ( ) type mockHttpClient struct { - mutex sync.RWMutex - called bool - OnGet func(url string) (*http.Response, error) + mutex sync.RWMutex + currNumCalls int + failAfterNumCalls int } func (m *mockHttpClient) Get(url string) (*http.Response, error) { m.mutex.Lock() - m.called = true + m.currNumCalls++ m.mutex.Unlock() - return m.OnGet(url) -} -func OnGetSuccess(url string) (*http.Response, error) { - compressed := snappy.Encode(nil, data.SigstoreBundleRaw) + if m.failAfterNumCalls > 0 && m.currNumCalls >= m.failAfterNumCalls { + return &http.Response{ + StatusCode: 500, + }, fmt.Errorf("failed to fetch with %s", url) + } + + var compressed []byte + compressed = snappy.Encode(compressed, data.SigstoreBundleRaw) return &http.Response{ StatusCode: 200, Body: io.NopCloser(bytes.NewReader(compressed)), }, nil } - -func OnGetFail(url string) (*http.Response, error) { - return &http.Response{ - StatusCode: 500, - }, fmt.Errorf("failed to fetch with %s", url) -} From e34e188ee2c38f345203cbfa1eeb000313e1763e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 10:43:24 -0700 Subject: [PATCH 0119/2338] add http client test constructors Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 37 +++++++++---------- .../attestation/api/mock_httpClient_test.go | 23 +++++++++++- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 0f91aff7ea1..23dcb77d9e9 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -21,14 +21,14 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { } l := io.NewTestHandler() - mockHTTPClient := &mockHttpClient{} + mockHTTPClient := SuccessHTTPClient() if hasNextPage { return &LiveClient{ githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, }, - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: l, } } @@ -37,7 +37,7 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccess, }, - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: l, } } @@ -139,11 +139,12 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { NumAttestations: 5, } + httpClient := SuccessHTTPClient() c := LiveClient{ githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, }, - httpClient: &mockHttpClient{}, + httpClient: &httpClient, logger: io.NewTestHandler(), } @@ -193,16 +194,14 @@ func TestFetchBundlesByURL(t *testing.T) { require.NoError(t, err) require.Len(t, fetched, 2) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) - require.Equal(t, 2, mockHTTPClient.currNumCalls) + require.Equal(t, 2, mockHTTPClient.TimesCalled()) } func TestFetchBundlesByURL_Fail(t *testing.T) { - mockHTTPClient := &mockHttpClient{ - failAfterNumCalls: 2, - } + mockHTTPClient := HTTPClientFailsAfterNumCalls(1) c := &LiveClient{ - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: io.NewTestHandler(), } @@ -215,10 +214,10 @@ func TestFetchBundlesByURL_Fail(t *testing.T) { } func TestFetchBundleByURL(t *testing.T) { - mockHTTPClient := &mockHttpClient{} + mockHTTPClient := SuccessHTTPClient() c := &LiveClient{ - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: io.NewTestHandler(), } @@ -226,16 +225,14 @@ func TestFetchBundleByURL(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.Equal(t, 1, mockHTTPClient.currNumCalls) + require.Equal(t, 1, mockHTTPClient.TimesCalled()) } func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { - mockHTTPClient := &mockHttpClient{ - failAfterNumCalls: 1, - } + mockHTTPClient := FailHTTPClient() c := &LiveClient{ - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: io.NewTestHandler(), } @@ -243,14 +240,14 @@ func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.Error(t, err) require.Nil(t, bundle) - require.Equal(t, 1, mockHTTPClient.currNumCalls) + require.Equal(t, 1, mockHTTPClient.TimesCalled()) } func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { - mockHTTPClient := &mockHttpClient{} + mockHTTPClient := SuccessHTTPClient() c := &LiveClient{ - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: io.NewTestHandler(), } @@ -258,7 +255,7 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.Equal(t, 0, mockHTTPClient.currNumCalls) + require.Equal(t, 0, mockHTTPClient.TimesCalled()) } func TestGetTrustDomain(t *testing.T) { diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 3218ed508fe..345d2c5c2bb 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -14,6 +14,7 @@ import ( type mockHttpClient struct { mutex sync.RWMutex currNumCalls int + alwaysFail bool failAfterNumCalls int } @@ -22,7 +23,7 @@ func (m *mockHttpClient) Get(url string) (*http.Response, error) { m.currNumCalls++ m.mutex.Unlock() - if m.failAfterNumCalls > 0 && m.currNumCalls >= m.failAfterNumCalls { + if m.alwaysFail || (m.failAfterNumCalls > 0 && m.currNumCalls > m.failAfterNumCalls) { return &http.Response{ StatusCode: 500, }, fmt.Errorf("failed to fetch with %s", url) @@ -35,3 +36,23 @@ func (m *mockHttpClient) Get(url string) (*http.Response, error) { Body: io.NopCloser(bytes.NewReader(compressed)), }, nil } + +func (m *mockHttpClient) TimesCalled() int { + return m.currNumCalls +} + +func FailHTTPClient() mockHttpClient { + return mockHttpClient{ + alwaysFail: true, + } +} + +func SuccessHTTPClient() mockHttpClient { + return mockHttpClient{} +} + +func HTTPClientFailsAfterNumCalls(numCalls int) mockHttpClient { + return mockHttpClient{ + failAfterNumCalls: numCalls, + } +} From ecf55c6c16f50279e98526b2a953630832e77a31 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 10:54:17 -0700 Subject: [PATCH 0120/2338] use mock to assert number of http calls Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 8 ++++---- pkg/cmd/attestation/api/mock_httpClient_test.go | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 23dcb77d9e9..3572689f8e6 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -194,7 +194,7 @@ func TestFetchBundlesByURL(t *testing.T) { require.NoError(t, err) require.Len(t, fetched, 2) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) - require.Equal(t, 2, mockHTTPClient.TimesCalled()) + mockHTTPClient.AssertNumberOfCalls(t, "OnGet", 2) } func TestFetchBundlesByURL_Fail(t *testing.T) { @@ -225,7 +225,7 @@ func TestFetchBundleByURL(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.Equal(t, 1, mockHTTPClient.TimesCalled()) + mockHTTPClient.AssertNumberOfCalls(t, "OnGet", 1) } func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { @@ -240,7 +240,7 @@ func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.Error(t, err) require.Nil(t, bundle) - require.Equal(t, 1, mockHTTPClient.TimesCalled()) + mockHTTPClient.AssertNumberOfCalls(t, "OnGet", 1) } func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { @@ -255,7 +255,7 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - require.Equal(t, 0, mockHTTPClient.TimesCalled()) + mockHTTPClient.AssertNotCalled(t, "OnGet") } func TestGetTrustDomain(t *testing.T) { diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 345d2c5c2bb..dd0c139d536 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -9,26 +9,33 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" "github.com/golang/snappy" + "github.com/stretchr/testify/mock" ) type mockHttpClient struct { + mock.Mock mutex sync.RWMutex currNumCalls int alwaysFail bool failAfterNumCalls int + OnGet func(url string) (*http.Response, error) } func (m *mockHttpClient) Get(url string) (*http.Response, error) { + m.On("OnGet").Return() m.mutex.Lock() m.currNumCalls++ m.mutex.Unlock() if m.alwaysFail || (m.failAfterNumCalls > 0 && m.currNumCalls > m.failAfterNumCalls) { + m.MethodCalled("OnGet") return &http.Response{ StatusCode: 500, }, fmt.Errorf("failed to fetch with %s", url) } + m.MethodCalled("OnGet") + var compressed []byte compressed = snappy.Encode(compressed, data.SigstoreBundleRaw) return &http.Response{ @@ -37,10 +44,6 @@ func (m *mockHttpClient) Get(url string) (*http.Response, error) { }, nil } -func (m *mockHttpClient) TimesCalled() int { - return m.currNumCalls -} - func FailHTTPClient() mockHttpClient { return mockHttpClient{ alwaysFail: true, From 9d88ca8cf8e14b86275f4c5612fae576ac597572 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 11:32:49 -0700 Subject: [PATCH 0121/2338] simplify mock http client Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 15 ++--- .../attestation/api/mock_httpClient_test.go | 59 ++++++++++++------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 3572689f8e6..feba1a9427e 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -181,9 +181,9 @@ func TestGetByDigest_Error(t *testing.T) { } func TestFetchBundlesByURL(t *testing.T) { - mockHTTPClient := &mockHttpClient{} + mockHTTPClient := SuccessHTTPClient() client := LiveClient{ - httpClient: mockHTTPClient, + httpClient: &mockHTTPClient, logger: io.NewTestHandler(), } @@ -194,11 +194,11 @@ func TestFetchBundlesByURL(t *testing.T) { require.NoError(t, err) require.Len(t, fetched, 2) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) - mockHTTPClient.AssertNumberOfCalls(t, "OnGet", 2) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) } func TestFetchBundlesByURL_Fail(t *testing.T) { - mockHTTPClient := HTTPClientFailsAfterNumCalls(1) + mockHTTPClient := FailsAfterNumCallsHTTPClient(1) c := &LiveClient{ httpClient: &mockHTTPClient, @@ -211,6 +211,7 @@ func TestFetchBundlesByURL_Fail(t *testing.T) { fetched, err := c.fetchBundlesByURL(attestations) require.Error(t, err) require.Nil(t, fetched) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterOneCall", 2) } func TestFetchBundleByURL(t *testing.T) { @@ -225,7 +226,7 @@ func TestFetchBundleByURL(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - mockHTTPClient.AssertNumberOfCalls(t, "OnGet", 1) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) } func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { @@ -240,7 +241,7 @@ func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.Error(t, err) require.Nil(t, bundle) - mockHTTPClient.AssertNumberOfCalls(t, "OnGet", 1) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) } func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { @@ -255,7 +256,7 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - mockHTTPClient.AssertNotCalled(t, "OnGet") + mockHTTPClient.AssertNotCalled(t, "OnGetSuccess") } func TestGetTrustDomain(t *testing.T) { diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index dd0c139d536..8cbc9175531 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "sync" "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" "github.com/golang/snappy" @@ -14,28 +13,48 @@ import ( type mockHttpClient struct { mock.Mock - mutex sync.RWMutex - currNumCalls int - alwaysFail bool - failAfterNumCalls int - OnGet func(url string) (*http.Response, error) } func (m *mockHttpClient) Get(url string) (*http.Response, error) { - m.On("OnGet").Return() - m.mutex.Lock() - m.currNumCalls++ - m.mutex.Unlock() + m.On("OnGetSuccess").Return() + m.MethodCalled("OnGetSuccess") - if m.alwaysFail || (m.failAfterNumCalls > 0 && m.currNumCalls > m.failAfterNumCalls) { - m.MethodCalled("OnGet") + var compressed []byte + compressed = snappy.Encode(compressed, data.SigstoreBundleRaw) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(compressed)), + }, nil +} + +type failHttpClient struct { + mock.Mock +} + +func (m *failHttpClient) Get(url string) (*http.Response, error) { + m.On("OnGetFail").Return() + m.MethodCalled("OnGetFail") + + return &http.Response{ + StatusCode: 500, + }, fmt.Errorf("failed to fetch with %s", url) +} + +type failAfterOneCallHttpClient struct { + mock.Mock +} + +func (m *failAfterOneCallHttpClient) Get(url string) (*http.Response, error) { + m.On("OnGetFailAfterOneCall").Return() + + if len(m.Calls) >= 1 { + m.MethodCalled("OnGetFailAfterOneCall") return &http.Response{ StatusCode: 500, }, fmt.Errorf("failed to fetch with %s", url) } - m.MethodCalled("OnGet") - + m.MethodCalled("OnGetFailAfterOneCall") var compressed []byte compressed = snappy.Encode(compressed, data.SigstoreBundleRaw) return &http.Response{ @@ -44,18 +63,14 @@ func (m *mockHttpClient) Get(url string) (*http.Response, error) { }, nil } -func FailHTTPClient() mockHttpClient { - return mockHttpClient{ - alwaysFail: true, - } +func FailHTTPClient() failHttpClient { + return failHttpClient{} } func SuccessHTTPClient() mockHttpClient { return mockHttpClient{} } -func HTTPClientFailsAfterNumCalls(numCalls int) mockHttpClient { - return mockHttpClient{ - failAfterNumCalls: numCalls, - } +func FailsAfterNumCallsHTTPClient(numCalls int) failAfterOneCallHttpClient { + return failAfterOneCallHttpClient{} } From 58ca3c69bbf4a84828773e844332f9026fa619d8 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 7 Jan 2025 10:36:15 -0800 Subject: [PATCH 0122/2338] Update triage.md to reflect FR experiment outcome --- docs/triage.md | 93 +++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/triage.md b/docs/triage.md index 9d54fcfe796..468cc41ccb3 100644 --- a/docs/triage.md +++ b/docs/triage.md @@ -1,27 +1,51 @@ # Triage role -As we get more issues and pull requests opened on the GitHub CLI, we've decided on a weekly rotation -triage role. The initial expectation is that the person in the role for the week spends no more than -2 hours a day on this work; we can refine that as needed. +As we get more issues and pull requests opened on the GitHub CLI, we've decided on a weekly rotation triage role as defined by our First Responder (FR) rotation. The primary responsibility of the FR during that week is to triage incoming issues from the Open Source community, as defined below. An issue is considered "triaged" when the `needs-triage` label is removed. -## Expectations for incoming issues +## Expectations for triaging incoming issues -Review and label [open issues missing either the `enhancement`, `bug`, or `docs` label](https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+-label%3Abug%2Cenhancement%2Cdocs+). +Review and label [open issues missing either the `enhancement`, `bug`, or `docs` label](https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+-label%3Abug%2Cenhancement%2Cdocs+) and the label(s) corresponding to the command space prefixed with `gh-`, such as `gh-pr` for the `gh pr` command set, or `gh-extension` for the `gh extension` command set. -Any issue that is accepted to be done as either an `enhancement`, `bug`, or `docs` requires explicit Acceptance Criteria in a comment on the issue before `needs-triage` label is removed. +Then, engage with the issue and community with the goal to remove the `needs-triage` label from the issue. The heuristics for triaging the different issue types are as follow: -To be considered triaged, `enhancement` issues require at least one of the following additional labels: +### Bugs -- `core`: reserved for the core CLI team -- `help wanted`: signal that we are accepting contributions for this -- `discuss`: add to our team's queue to discuss during a sync -- `needs-investigation`: work that requires a mystery be solved by the core team before it can move forward -- `needs-user-input`: we need more information from our users before the task can move forward +To be considered triaged, `bug` issues require the following: -To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`, which are defined as follows: - - `p1`: Affects a large population and inhibits work - - `p2`: Affects more than a few users but doesn't prevent core functions - - `p3`: Affects a small number of users or is largely cosmetic +- A severity label `p1`, `p2`, and `p3` +- Clearly defined Acceptance Criteria, added to the Issue as a standalone comment (see [example](https://github.com/cli/cli/issues/9469#issuecomment-2292315743)) + +#### Bug severities + +| Severity | Description | +| - | - | +| `p1` | Affects a large population and inhibits work | +| `p2` | Affects more than a few users but doesn't prevent core functions | +| `p3` | Affects a small number of users or is largely cosmetic | + +### Enhancements + +To be considered triaged, `enhancement` issues require either + +- Clearly defined Acceptance Criteria as above +- The `needs-investigation` or `needs-design` label with a clearly defined set of open questions to be investigated. + +### Docs + +To be considered triaged, `docs` issues require clearly defined Acceptance Criteria, as defined above + +## Additional triaging processes and labels + +Before removing the `needs-triage` label, consider adding any of the following labels below. + +| Label | Description | +| - | - | +| `discuss` | Some issues require discussion with the internal team. Adding this label will automatically open up an internal discussion with the team to facilitate this discussion. | +| `core` | Defines what we would like to do internally. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. | +| `good first issue` | Used to denote when an issue may be a good candidate for a first-time contributor to the CLI. These are usually small and well defined issues. | +| `help wanted` | Defines what we feel the community could solve should they care to contribute, respectively. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. | +| `invalid` | Added to spam and abusive issues. | +| `needs-user-input` | Some issues require a lot of back and forth with a contributor before they can be successfully triaged. After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. | ## Expectations for community pull requests @@ -29,36 +53,13 @@ All incoming pull requests are assigned to one of the engineers for review on a The person in a triage role for a week could take a glance at these pull requests, mostly to see whether the changeset is feasible and to allow the associated CI run for new contributors. -## Issue triage flowchart - -- can this be closed outright? - - e.g. spam/junk - - add the `invalid` label - - close without comment -- do we not want to do it? - - e.g. we have already discussed not wanting to do or it's a duplicate issue - - add the appropriate label (e.g. `duplicate`) - - comment and close -- do we want external contribution for this? - - e.g. the task is relatively straightforward, but the core team does not have the bandwidth to take it on - - ensure that the thread contains all the context necessary for someone new to pick this up - - add the `help wanted` label - - consider adding `good first issue` label -- do we want external design contribution for this? - - e.g. the task is worthwhile, but needs design work to flesh out the details before implementation and the core team does not have the bandwidth to take it on - - ensure that the thread contains all the context necessary for someone new to pick this up - - add both the `help wanted` and `needs-design` labels -- do we want to do it? - - add the `core` label - - comment acknowledging that -- is it intriguing, but requires discussion? - - Add the `discuss` label - - Add the `needs-investigation` label if engineering research is required before action can be taken -- does it need more info from the issue author? - - ask the user for details - - add the `needs-user-input` label -- is it a usage/support question? - - Convert the Issue to a Discussion +## Spam and abuse + +The primary goal of triaging spam and abuse is to remove distracting and offensive content from our community. + +We get a lot of spam. Whenever you determine an issue as spam, add the `invalid` label and close it as "won't do". For spammy comments, simply mark them as spam using GitHub's built-in spam feature. + +Abusive contributions are defined by our [Code of Conduct](../.github/CODE-OF-CONDUCT.md). Any contribution you determine abusive should be removed. Repeat offenses or particularly offensive abuse should be reported using GitHub's reporting features. If an entire issue is abusive, label it as `invalid` and close as "won't do". ## Weekly PR audit From 7838e912b657f71371b96326790c7d5a0a84ba39 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 11:37:02 -0700 Subject: [PATCH 0123/2338] more mock http client cleanup Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 36 +++++++++---------- .../attestation/api/mock_httpClient_test.go | 12 ------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index feba1a9427e..9b8dabc0092 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -21,14 +21,14 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { } l := io.NewTestHandler() - mockHTTPClient := SuccessHTTPClient() + httpClient := &mockHttpClient{} if hasNextPage { return &LiveClient{ githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, }, - httpClient: &mockHTTPClient, + httpClient: httpClient, logger: l, } } @@ -37,7 +37,7 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTSuccess, }, - httpClient: &mockHTTPClient, + httpClient: httpClient, logger: l, } } @@ -139,12 +139,12 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { NumAttestations: 5, } - httpClient := SuccessHTTPClient() + httpClient := &mockHttpClient{} c := LiveClient{ githubAPI: mockAPIClient{ OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, }, - httpClient: &httpClient, + httpClient: httpClient, logger: io.NewTestHandler(), } @@ -181,9 +181,9 @@ func TestGetByDigest_Error(t *testing.T) { } func TestFetchBundlesByURL(t *testing.T) { - mockHTTPClient := SuccessHTTPClient() + httpClient := &mockHttpClient{} client := LiveClient{ - httpClient: &mockHTTPClient, + httpClient: httpClient, logger: io.NewTestHandler(), } @@ -194,14 +194,14 @@ func TestFetchBundlesByURL(t *testing.T) { require.NoError(t, err) require.Len(t, fetched, 2) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) + httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) } func TestFetchBundlesByURL_Fail(t *testing.T) { - mockHTTPClient := FailsAfterNumCallsHTTPClient(1) + httpClient := &failAfterOneCallHttpClient{} c := &LiveClient{ - httpClient: &mockHTTPClient, + httpClient: httpClient, logger: io.NewTestHandler(), } @@ -211,14 +211,14 @@ func TestFetchBundlesByURL_Fail(t *testing.T) { fetched, err := c.fetchBundlesByURL(attestations) require.Error(t, err) require.Nil(t, fetched) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterOneCall", 2) + httpClient.AssertNumberOfCalls(t, "OnGetFailAfterOneCall", 2) } func TestFetchBundleByURL(t *testing.T) { - mockHTTPClient := SuccessHTTPClient() + httpClient := &mockHttpClient{} c := &LiveClient{ - httpClient: &mockHTTPClient, + httpClient: httpClient, logger: io.NewTestHandler(), } @@ -226,14 +226,14 @@ func TestFetchBundleByURL(t *testing.T) { bundle, err := c.fetchBundleByURL(&attestation) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) + httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) } func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { - mockHTTPClient := FailHTTPClient() + mockHTTPClient := &failHttpClient{} c := &LiveClient{ - httpClient: &mockHTTPClient, + httpClient: mockHTTPClient, logger: io.NewTestHandler(), } @@ -245,10 +245,10 @@ func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { } func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { - mockHTTPClient := SuccessHTTPClient() + mockHTTPClient := &mockHttpClient{} c := &LiveClient{ - httpClient: &mockHTTPClient, + httpClient: mockHTTPClient, logger: io.NewTestHandler(), } diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 8cbc9175531..4dc34fbf8e7 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -62,15 +62,3 @@ func (m *failAfterOneCallHttpClient) Get(url string) (*http.Response, error) { Body: io.NopCloser(bytes.NewReader(compressed)), }, nil } - -func FailHTTPClient() failHttpClient { - return failHttpClient{} -} - -func SuccessHTTPClient() mockHttpClient { - return mockHttpClient{} -} - -func FailsAfterNumCallsHTTPClient(numCalls int) failAfterOneCallHttpClient { - return failAfterOneCallHttpClient{} -} From 3c0280c4d7d1621412093f78e6870b0129fbfb4c Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 11:51:23 -0700 Subject: [PATCH 0124/2338] undo name change for now Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 8022a49434a..5bca397002d 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -102,19 +102,19 @@ func GetRemoteAttestations(client api.Client, params FetchRemoteAttestationsPara // check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo. // If Repo is not set, the field will remain empty. It will not be populated using the value of Owner. if params.Repo != "" { - bundles, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit) + attestations, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Repo, err) } - return bundles, nil + return attestations, nil } else if params.Owner != "" { - bundles, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit) + attestations, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Owner, err) } - return bundles, nil + return attestations, nil } return nil, fmt.Errorf("owner or repo must be provided") } From 37e0969f20cc6881ab7ccf3b4c675d13ea3a5dbd Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 11:51:51 -0700 Subject: [PATCH 0125/2338] remove spaces Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 5bca397002d..fdfd667bd1b 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -106,14 +106,12 @@ func GetRemoteAttestations(client api.Client, params FetchRemoteAttestationsPara if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Repo, err) } - return attestations, nil } else if params.Owner != "" { attestations, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit) if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Owner, err) } - return attestations, nil } return nil, fmt.Errorf("owner or repo must be provided") From 0a602fae073326f882834987492d7433c88a7f36 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 11:54:02 -0700 Subject: [PATCH 0126/2338] undo other name change Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 4 ++-- pkg/cmd/attestation/api/client_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index d0ef3f21bf9..dc3ab2cb4f2 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -60,14 +60,14 @@ func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClien } } -func (c *LiveClient) buildRepoAndDigestURL(repo, digest string) string { +func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { repo = strings.Trim(repo, "/") return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest) } // GetByRepoAndDigest fetches the attestation by repo and digest func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { - url := c.buildRepoAndDigestURL(repo, digest) + url := c.BuildRepoAndDigestURL(repo, digest) attestations, err := c.getAttestations(url, repo, digest, limit) if err != nil { return nil, err diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 9b8dabc0092..7a01809cf21 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -55,7 +55,7 @@ func TestGetURL(t *testing.T) { } for _, data := range testData { - s := c.buildRepoAndDigestURL(data.repo, data.digest) + s := c.BuildRepoAndDigestURL(data.repo, data.digest) require.Equal(t, data.expected, s) } } From 258c69cd266e725eb26de266281aa737241d4fbc Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 11:56:05 -0700 Subject: [PATCH 0127/2338] undo more name chanages Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index dc3ab2cb4f2..02b4d67587f 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -81,14 +81,14 @@ func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Atte return bundles, nil } -func (c *LiveClient) buildOwnerAndDigestURL(owner, digest string) string { +func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { owner = strings.Trim(owner, "/") return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest) } // GetByOwnerAndDigest fetches attestation by owner and digest func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { - url := c.buildOwnerAndDigestURL(owner, digest) + url := c.BuildOwnerAndDigestURL(owner, digest) attestations, err := c.getAttestations(url, owner, digest, limit) if err != nil { return nil, err From f46cccbab4ecfdcfd9dd61743f2b53d19cf375ea Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 12:03:49 -0700 Subject: [PATCH 0128/2338] comment Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 02b4d67587f..20df51165ea 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -191,6 +191,7 @@ func (c *LiveClient) fetchBundlesByURL(attestations []*Attestation) ([]*Attestat } func (c *LiveClient) fetchBundleByURL(a *Attestation) (*bundle.Bundle, error) { + // for now, we fallback to the bundle field if the bundle URL is empty if a.BundleURL == "" { c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") return a.Bundle, nil From 1e91828cdfd06df2211e90eea8f49982f77cc0ad Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 13:36:50 -0700 Subject: [PATCH 0129/2338] remove commented out code Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 3171b12787d..73987d8a290 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -333,7 +333,6 @@ func buildTableVerifyContent(tenant string, results []*verification.AttestationP return nil, err } - //sourceRepoURI := res.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI buildConfigURI := res.VerificationResult.Signature.Certificate.Extensions.BuildConfigURI sourceRepoAndOrg, sourceWorkflow, err := extractAttestationDetail(tenant, buildConfigURI) if err != nil { From 42cb2547cdcb8bf089bbdd055f7a0ac8114cc37f Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 14:13:50 -0700 Subject: [PATCH 0130/2338] remove old comment Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 20df51165ea..9c5979d46f6 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -209,7 +209,6 @@ func (c *LiveClient) fetchBundleByURL(a *Attestation) (*bundle.Bundle, error) { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - // parse the URL to get the host and path body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read blob storage response body: %w", err) From bf6fdbdfd214c97f45d2610289236ae3537d3ea4 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 7 Jan 2025 11:31:46 -0800 Subject: [PATCH 0131/2338] Remove named returns from ReadBranchConfig and surface errors --- git/client.go | 14 ++++++++++---- git/client_test.go | 2 +- pkg/cmd/pr/create/create.go | 6 +++++- pkg/cmd/pr/create/create_test.go | 2 +- pkg/cmd/pr/shared/finder.go | 3 ++- pkg/cmd/pr/status/status.go | 2 +- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/git/client.go b/git/client.go index 1a6d9ae7f08..501d1ba9d2f 100644 --- a/git/client.go +++ b/git/client.go @@ -377,16 +377,22 @@ 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) { + var cfg BranchConfig + 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 + return cfg, err } + out, err := cmd.Output() if err != nil { - return + return cfg, err } for _, line := range outputLines(out) { @@ -412,7 +418,7 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc cfg.MergeBase = parts[1] } } - return + return cfg, nil } // SetBranchConfig sets the named value on the given branch. diff --git a/git/client_test.go b/git/client_test.go index fff1397f397..182ce67eafe 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -747,7 +747,7 @@ func TestClientReadBranchConfig(t *testing.T) { GitPath: "path/to/git", commandContext: cmdCtx, } - branchConfig := client.ReadBranchConfig(context.Background(), "trunk") + branchConfig, _ := client.ReadBranchConfig(context.Background(), "trunk") assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) assert.Equal(t, tt.wantBranchConfig, branchConfig) }) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index db160b419e3..638d8eed4bc 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -680,7 +680,11 @@ 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 { + // We can still proceed without the branch config, so print to stderr but continue + fmt.Fprintf(opts.IO.ErrOut, "Error reading git branch config: %v\n", err) + } if isPushEnabled { // determine whether the head branch is already pushed to a remote if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found { diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 6c0ff11e226..66fe1c39a98 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1717,7 +1717,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) { GhPath: "some/path/gh", GitPath: "some/path/git", } - headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature") + headBranchConfig, _ := gitClient.ReadBranchConfig(ctx.Background(), "feature") ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", headBranchConfig) assert.Equal(t, tt.expectedTrackingRef, ref) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index e4a70eb2230..3bc197b75df 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -59,7 +59,8 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { httpClient: factory.HttpClient, progress: factory.IOStreams, branchConfig: func(s string) git.BranchConfig { - return factory.GitClient.ReadBranchConfig(context.Background(), s) + branchConfig, _ := factory.GitClient.ReadBranchConfig(context.Background(), s) + return branchConfig }, } } diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 27666461d4c..2613a3a6b01 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -186,7 +186,7 @@ func statusRun(opts *StatusOptions) error { 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) + branchConfig, _ := gitClient.ReadBranchConfig(context.Background(), prHeadRef) // the branch is configured to merge a special PR head ref prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`) From f43da8ea9a4c4c3caccbaa082232b36e26fad380 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 7 Jan 2025 12:19:13 -0800 Subject: [PATCH 0132/2338] Refactor ReadBranchConfig for test coverage of newly returned erros --- git/client.go | 22 +++++++++-- git/client_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/git/client.go b/git/client.go index 501d1ba9d2f..b356e9ef5b1 100644 --- a/git/client.go +++ b/git/client.go @@ -383,19 +383,33 @@ func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte, func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchConfig, error) { var cfg BranchConfig + branchConfigOutput, err := c.readGitBranchConfig(ctx, branch) + if err != nil { + return cfg, err + } + + return c.createBranchConfig(outputLines(branchConfigOutput)), nil +} + +func (c *Client) readGitBranchConfig(ctx context.Context, branch string) ([]byte, 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 cfg, err + return []byte{}, err } out, err := cmd.Output() if err != nil { - return cfg, err + return []byte{}, err } - for _, line := range outputLines(out) { + return out, nil +} + +func (c *Client) createBranchConfig(gitBranchConfigOutput []string) BranchConfig { + var cfg BranchConfig + for _, line := range gitBranchConfigOutput { parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { continue @@ -418,7 +432,7 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon cfg.MergeBase = parts[1] } } - return cfg, nil + return cfg } // SetBranchConfig sets the named value on the given branch. diff --git a/git/client_test.go b/git/client_test.go index 182ce67eafe..448908591cc 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -754,6 +754,100 @@ func TestClientReadBranchConfig(t *testing.T) { } } +func Test_readGitBranchConfig(t *testing.T) { + tests := []struct { + name string + branch string + cmdExitCode int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErr *GitError + }{ + { + name: "read branch config", + branch: "trunk", + 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)$`, + }, + { + name: "command failure", + branch: "trunk", + wantCmdArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`, + cmdExitCode: 1, + cmdStderr: "error", + wantErr: &GitError{ExitCode: 1, Stderr: "error"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitCode, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + out, err := client.readGitBranchConfig(context.Background(), tt.branch) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.ExitCode, err.(*GitError).ExitCode) + assert.Equal(t, tt.wantErr.Stderr, err.(*GitError).Stderr) + } else { + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.Equal(t, tt.cmdStdout, string(out)) + } + }) + } +} + +func Test_createBranchConfig(t *testing.T) { + tests := []struct { + name string + gitBranchConfigOutput []string + wantBranchConfig BranchConfig + }{ + { + name: "remote branch", + gitBranchConfigOutput: []string{"branch.trunk.remote origin"}, + wantBranchConfig: BranchConfig{ + RemoteName: "origin", + }, + }, + { + name: "merge ref", + gitBranchConfigOutput: []string{"branch.trunk.merge refs/heads/trunk"}, + wantBranchConfig: BranchConfig{ + MergeRef: "refs/heads/trunk", + }, + }, + { + name: "merge base", + gitBranchConfigOutput: []string{"branch.trunk.gh-merge-base gh-merge-base"}, + wantBranchConfig: BranchConfig{ + MergeBase: "gh-merge-base", + }, + }, + { + name: "remote, merge ref, and merge base all specified", + gitBranchConfigOutput: []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", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := Client{} + branchConfig := client.createBranchConfig(tt.gitBranchConfigOutput) + assert.Equal(t, tt.wantBranchConfig, branchConfig) + }) + } +} + func TestClientDeleteLocalTag(t *testing.T) { tests := []struct { name string From c83cf32cff2027f6506203aca5858137fbe2ea72 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 7 Jan 2025 13:41:53 -0800 Subject: [PATCH 0133/2338] Remove named return values from prSelectorForCurrentBranch --- pkg/cmd/pr/status/status.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 2613a3a6b01..9245b1cdc11 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -184,17 +184,17 @@ 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 +func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (int, string, error) { branchConfig, _ := gitClient.ReadBranchConfig(context.Background(), prHeadRef) // 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]) + return prNumber, prHeadRef, err } + var err error var branchOwner string if branchConfig.RemoteURL != nil { // the branch merges from a remote specified by URL @@ -208,6 +208,7 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface } } + selector := prHeadRef if branchOwner != "" { if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") @@ -218,7 +219,7 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface } } - return + return 0, selector, err } func totalApprovals(pr *api.PullRequest) int { From 51a74aed1daa4cff4452d847a79a4842ce17ec86 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 15:14:23 -0700 Subject: [PATCH 0134/2338] Update pkg/cmd/attestation/api/client.go Co-authored-by: Phill MV --- pkg/cmd/attestation/api/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 9c5979d46f6..ca6c0f3d56f 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -172,7 +172,7 @@ func (c *LiveClient) fetchBundlesByURL(attestations []*Attestation) ([]*Attestat g := errgroup.Group{} for i, a := range attestations { g.Go(func() error { - b, err := c.fetchBundleByURL(a) + b, err := c.GetBundle(url) if err != nil { return fmt.Errorf("failed to fetch bundle with URL: %w", err) } From 8d89dd97fdb487f8ad64d386de1d25d6ff3d36c8 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 15:14:53 -0700 Subject: [PATCH 0135/2338] Update pkg/cmd/attestation/api/client.go Co-authored-by: Phill MV --- pkg/cmd/attestation/api/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index ca6c0f3d56f..bf49756be1b 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -167,7 +167,7 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At return attestations, nil } -func (c *LiveClient) fetchBundlesByURL(attestations []*Attestation) ([]*Attestation, error) { +func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([]*Attestation, error) { fetched := make([]*Attestation, len(attestations)) g := errgroup.Group{} for i, a := range attestations { From 33d0002d214c4a94b063ba3cad36aa6f0ec24aaf Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 7 Jan 2025 15:22:02 -0700 Subject: [PATCH 0136/2338] update tests to use new function name Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 25 +++++++++-------- pkg/cmd/attestation/api/client_test.go | 37 +++++++++----------------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index bf49756be1b..f8d0509ee63 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -73,7 +73,7 @@ func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Atte return nil, err } - bundles, err := c.fetchBundlesByURL(attestations) + bundles, err := c.fetchBundleFromAttestations(attestations) if err != nil { return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) } @@ -98,7 +98,7 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At return nil, newErrNoAttestations(owner, digest) } - bundles, err := c.fetchBundlesByURL(attestations) + bundles, err := c.fetchBundleFromAttestations(attestations) if err != nil { return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) } @@ -172,7 +172,16 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ g := errgroup.Group{} for i, a := range attestations { g.Go(func() error { - b, err := c.GetBundle(url) + // for now, we fallback to the bundle field if the bundle URL is empty + if a.BundleURL == "" { + c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") + fetched[i] = &Attestation{ + Bundle: a.Bundle, + } + return nil + } + + b, err := c.GetBundle(a.BundleURL) if err != nil { return fmt.Errorf("failed to fetch bundle with URL: %w", err) } @@ -190,16 +199,10 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ return fetched, nil } -func (c *LiveClient) fetchBundleByURL(a *Attestation) (*bundle.Bundle, error) { - // for now, we fallback to the bundle field if the bundle URL is empty - if a.BundleURL == "" { - c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") - return a.Bundle, nil - } - +func (c *LiveClient) GetBundle(url string) (*bundle.Bundle, error) { c.logger.VerbosePrintf("Fetching attestation bundle with bundle URL\n\n") - resp, err := c.httpClient.Get(a.BundleURL) + resp, err := c.httpClient.Get(url) if err != nil { return nil, err } diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 7a01809cf21..fcceb33d7af 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -180,7 +180,7 @@ func TestGetByDigest_Error(t *testing.T) { require.Nil(t, attestations) } -func TestFetchBundlesByURL(t *testing.T) { +func TestFetchBundleFromAttestations(t *testing.T) { httpClient := &mockHttpClient{} client := LiveClient{ httpClient: httpClient, @@ -190,14 +190,14 @@ func TestFetchBundlesByURL(t *testing.T) { att1 := makeTestAttestation() att2 := makeTestAttestation() attestations := []*Attestation{&att1, &att2} - fetched, err := client.fetchBundlesByURL(attestations) + fetched, err := client.fetchBundleFromAttestations(attestations) require.NoError(t, err) require.Len(t, fetched, 2) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) } -func TestFetchBundlesByURL_Fail(t *testing.T) { +func TestFetchBundleFromAttestations_Fail(t *testing.T) { httpClient := &failAfterOneCallHttpClient{} c := &LiveClient{ @@ -208,28 +208,13 @@ func TestFetchBundlesByURL_Fail(t *testing.T) { att1 := makeTestAttestation() att2 := makeTestAttestation() attestations := []*Attestation{&att1, &att2} - fetched, err := c.fetchBundlesByURL(attestations) + fetched, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) require.Nil(t, fetched) httpClient.AssertNumberOfCalls(t, "OnGetFailAfterOneCall", 2) } -func TestFetchBundleByURL(t *testing.T) { - httpClient := &mockHttpClient{} - - c := &LiveClient{ - httpClient: httpClient, - logger: io.NewTestHandler(), - } - - attestation := makeTestAttestation() - bundle, err := c.fetchBundleByURL(&attestation) - require.NoError(t, err) - require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) - httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) -} - -func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { +func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { mockHTTPClient := &failHttpClient{} c := &LiveClient{ @@ -237,8 +222,9 @@ func TestFetchBundleByURL_FetchByURLFail(t *testing.T) { logger: io.NewTestHandler(), } - attestation := makeTestAttestation() - bundle, err := c.fetchBundleByURL(&attestation) + a := makeTestAttestation() + attestations := []*Attestation{&a} + bundle, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) require.Nil(t, bundle) mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) @@ -252,10 +238,11 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { logger: io.NewTestHandler(), } - attestation := Attestation{Bundle: data.SigstoreBundle(t)} - bundle, err := c.fetchBundleByURL(&attestation) + a := Attestation{Bundle: data.SigstoreBundle(t)} + attestations := []*Attestation{&a} + fetched, err := c.fetchBundleFromAttestations(attestations) require.NoError(t, err) - require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", bundle.GetMediaType()) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) mockHTTPClient.AssertNotCalled(t, "OnGetSuccess") } From 01371508486e86f5c4b772f0eee777e74cfa9b70 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 7 Jan 2025 14:56:27 -0800 Subject: [PATCH 0137/2338] Refactor Test_readGitBranchConfig for easier parsing --- git/client.go | 10 +++---- git/client_test.go | 66 ++++++++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/git/client.go b/git/client.go index b356e9ef5b1..20a489a2911 100644 --- a/git/client.go +++ b/git/client.go @@ -388,23 +388,23 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon return cfg, err } - return c.createBranchConfig(outputLines(branchConfigOutput)), nil + return c.createBranchConfig(branchConfigOutput), nil } -func (c *Client) readGitBranchConfig(ctx context.Context, branch string) ([]byte, error) { +func (c *Client) readGitBranchConfig(ctx context.Context, branch string) ([]string, 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 []byte{}, err + return []string{}, err } out, err := cmd.Output() if err != nil { - return []byte{}, err + return []string{}, err } - return out, nil + return outputLines(out), nil } func (c *Client) createBranchConfig(gitBranchConfigOutput []string) BranchConfig { diff --git a/git/client_test.go b/git/client_test.go index 448908591cc..0930a6f7232 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -756,43 +756,57 @@ func TestClientReadBranchConfig(t *testing.T) { func Test_readGitBranchConfig(t *testing.T) { tests := []struct { - name string - branch string - cmdExitCode int - cmdStdout string - cmdStderr string - wantCmdArgs string - wantErr *GitError + name string + branch string + cmdExitStatus int + cmdStdout string + cmdStderr string + expectedOutput []string + expectedError *GitError }{ { - name: "read branch config", - branch: "trunk", - 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)$`, - }, - { - name: "command failure", - branch: "trunk", - wantCmdArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`, - cmdExitCode: 1, - cmdStderr: "error", - wantErr: &GitError{ExitCode: 1, Stderr: "error"}, + name: "read branch config", + branch: "trunk", + cmdExitStatus: 0, + cmdStdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk", + expectedOutput: []string{ + "branch.trunk.remote origin", + "branch.trunk.merge refs/heads/trunk", + "branch.trunk.gh-merge-base trunk"}, + expectedError: nil, + }, + { + name: "command failure", + branch: "trunk", + cmdExitStatus: 1, + cmdStderr: "git error message", + expectedOutput: []string{}, + expectedError: &GitError{ + ExitCode: 1, + Stderr: "git error message", + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitCode, tt.cmdStdout, tt.cmdStderr) + commandString := fmt.Sprintf("path/to/git config --get-regexp ^branch\\.%s\\.(remote|merge|gh-merge-base)$", tt.branch) + cmdCtx := createMockedCommandContext(t, map[args]commandResult{ + args(commandString): { + ExitStatus: tt.cmdExitStatus, + Stdout: tt.cmdStdout, + Stderr: tt.cmdStderr, + }, + }) client := Client{ GitPath: "path/to/git", commandContext: cmdCtx, } + out, err := client.readGitBranchConfig(context.Background(), tt.branch) - if tt.wantErr != nil { - assert.Equal(t, tt.wantErr.ExitCode, err.(*GitError).ExitCode) - assert.Equal(t, tt.wantErr.Stderr, err.(*GitError).Stderr) - } else { - assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) - assert.Equal(t, tt.cmdStdout, string(out)) + assert.Equal(t, tt.expectedOutput, out) + if err != nil { + assert.Equal(t, tt.cmdStderr, err.(*GitError).Stderr) + assert.Equal(t, tt.cmdExitStatus, err.(*GitError).ExitCode) } }) } From 47c391b24682735d341e4b7e59d8bc0651da9069 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 7 Jan 2025 15:42:17 -0800 Subject: [PATCH 0138/2338] PR review edits --- docs/triage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/triage.md b/docs/triage.md index 468cc41ccb3..7f8bbbd7b07 100644 --- a/docs/triage.md +++ b/docs/triage.md @@ -45,7 +45,7 @@ Before removing the `needs-triage` label, consider adding any of the following l | `good first issue` | Used to denote when an issue may be a good candidate for a first-time contributor to the CLI. These are usually small and well defined issues. | | `help wanted` | Defines what we feel the community could solve should they care to contribute, respectively. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. | | `invalid` | Added to spam and abusive issues. | -| `needs-user-input` | Some issues require a lot of back and forth with a contributor before they can be successfully triaged. After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. | +| `needs-user-input` | After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. | ## Expectations for community pull requests @@ -59,7 +59,7 @@ The primary goal of triaging spam and abuse is to remove distracting and offensi We get a lot of spam. Whenever you determine an issue as spam, add the `invalid` label and close it as "won't do". For spammy comments, simply mark them as spam using GitHub's built-in spam feature. -Abusive contributions are defined by our [Code of Conduct](../.github/CODE-OF-CONDUCT.md). Any contribution you determine abusive should be removed. Repeat offenses or particularly offensive abuse should be reported using GitHub's reporting features. If an entire issue is abusive, label it as `invalid` and close as "won't do". +Abusive contributions are defined by our [Code of Conduct](../.github/CODE-OF-CONDUCT.md). Any contribution you determine abusive should be removed. Repeat offenses or particularly offensive abuse should be reported using GitHub's reporting features and the user blocked. If an entire issue is abusive, label it as `invalid` and close as "won't do". ## Weekly PR audit From a8f0aaeb3d3c89ab4d6e2eca054216ca44dd77ae Mon Sep 17 00:00:00 2001 From: danochoa Date: Wed, 8 Jan 2025 02:22:21 -0600 Subject: [PATCH 0139/2338] tentative updates for acceptance criteria - pending confirmation re description in prompt --- pkg/cmd/gist/delete/delete.go | 19 +++- pkg/cmd/gist/delete/delete_test.go | 150 +++++++++++++++++++++-------- 2 files changed, 124 insertions(+), 45 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index 23e929d5b26..a2b552a3a56 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -35,7 +35,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd := &cobra.Command{ - Use: "delete [ | ]", + Use: "delete { | }", Short: "Delete a gist", Long: heredoc.Docf(` Delete a GitHub gist. @@ -53,9 +53,18 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co `), Args: cobra.MaximumNArgs(1), RunE: func(c *cobra.Command, args []string) error { + 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) } @@ -97,9 +106,11 @@ func deleteRun(opts *DeleteOptions) error { } if !opts.Confirmed { - err = opts.Prompter.ConfirmDeletion("delete") - + confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete \"%s\" gist?", gistID), false) if err != nil { + return err + } + if !confirmed { return cmdutil.CancelError } } @@ -128,7 +139,7 @@ func deleteGist(apiClient *api.Client, hostname string, gistID string, opts *Del if opts.IO.IsStdoutTTY() { cs := opts.IO.ColorScheme() fmt.Fprintf(opts.IO.Out, - "%s Deleted gist %s\n", + "%s Gist \"%s\" deleted\n", cs.SuccessIcon(), gistID) } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index fec3b4ffd5e..237e3f76886 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -19,36 +19,75 @@ import ( 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: "no ID supplied", + name: "valid selector, no ID supplied", cli: "", - wants: DeleteOptions{ + tty: true, + want: DeleteOptions{ Selector: "", }, }, { - name: "yes flag", + name: "no ID supplied with --yes", + cli: "--yes", + tty: true, + want: DeleteOptions{ + Selector: "", + }, + }, + { + name: "selector with --yes, no tty", cli: "123 --yes", - wants: DeleteOptions{ + 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) @@ -64,9 +103,13 @@ 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) }) } } @@ -75,6 +118,7 @@ func Test_deleteRun(t *testing.T) { tests := []struct { name string opts *DeleteOptions + cancel bool httpStubs func(*httpmock.Registry) mockGistList bool wantErr bool @@ -91,7 +135,7 @@ func Test_deleteRun(t *testing.T) { httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "✓ Deleted gist 1234\n", + wantStdout: "✓ Gist \"1234\" deleted\n", wantStderr: "", }, { @@ -105,7 +149,7 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "✓ Deleted gist 1234\n", + wantStdout: "✓ Gist \"1234\" deleted\n", wantStderr: "", }, { @@ -119,7 +163,7 @@ func Test_deleteRun(t *testing.T) { httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "✓ Deleted gist 1234\n", + wantStdout: "✓ Gist \"1234\" deleted\n", wantStderr: "", }, { @@ -134,9 +178,29 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "✓ Deleted gist 1234\n", + wantStdout: "✓ Gist \"1234\" deleted\n", wantStderr: "", }, + { + name: "cancel delete with id", + opts: &DeleteOptions{ + Selector: "1234", + }, + cancel: true, + wantErr: true, + wantStdout: "", + wantStderr: "", + }, + { + name: "cancel delete with prompt", + opts: &DeleteOptions{ + Selector: "", + }, + cancel: true, + wantErr: true, + wantStdout: "", + wantStderr: "", + }, { name: "not found", opts: &DeleteOptions{ @@ -153,43 +217,47 @@ func Test_deleteRun(t *testing.T) { } for _, tt := range tests { - reg := &httpmock.Registry{} - tt.httpStubs(reg) pm := prompter.NewMockPrompter(t) if !tt.opts.Confirmed { - pm.RegisterConfirmDeletion("Type delete to confirm deletion:", func(_ string) error { - return nil + pm.RegisterConfirm("Delete \"1234\" gist?", func(_ string, _ bool) (bool, error) { + return !tt.cancel, nil }) } - 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": [ - { - "name": "1234", - "files": [{ "name": "cool.txt" }], - "description": "", - "updatedAt": "%s", - "isPublic": true - } - ] } } } }`, - sixHoursAgo.Format(time.RFC3339), - )), - ) - - pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { - return 0, nil - }) + reg := &httpmock.Registry{} + if !tt.cancel { + tt.httpStubs(reg) + 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": [ + { + "name": "1234", + "files": [{ "name": "cool.txt" }], + "description": "", + "updatedAt": "%s", + "isPublic": true + } + ] } } } }`, + sixHoursAgo.Format(time.RFC3339), + )), + ) + + pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []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 } From 8ad877b188762e7a613ddc68ce220f4e7144a856 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 8 Jan 2025 08:38:43 -0700 Subject: [PATCH 0140/2338] add check for invalid attestation Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 5 +++++ pkg/cmd/attestation/api/client_test.go | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index f8d0509ee63..37579b7bccf 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -172,6 +172,10 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ g := errgroup.Group{} for i, a := range attestations { g.Go(func() error { + if a.Bundle == nil && a.BundleURL == "" { + return fmt.Errorf("attestation has no bundle or bundle URL") + } + // for now, we fallback to the bundle field if the bundle URL is empty if a.BundleURL == "" { c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") @@ -181,6 +185,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ return nil } + // otherwise fetch the bundle with the provided URL b, err := c.GetBundle(a.BundleURL) if err != nil { return fmt.Errorf("failed to fetch bundle with URL: %w", err) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index fcceb33d7af..65f8d59ca3c 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -197,6 +197,20 @@ func TestFetchBundleFromAttestations(t *testing.T) { httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) } +func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { + httpClient := &mockHttpClient{} + client := LiveClient{ + httpClient: httpClient, + logger: io.NewTestHandler(), + } + + att1 := Attestation{} + attestations := []*Attestation{&att1} + fetched, err := client.fetchBundleFromAttestations(attestations) + require.Error(t, err) + require.Nil(t, fetched, 2) +} + func TestFetchBundleFromAttestations_Fail(t *testing.T) { httpClient := &failAfterOneCallHttpClient{} From 1c16e12a666e3fe90cdcc894eaf622686e97095a Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 8 Jan 2025 11:16:23 -0700 Subject: [PATCH 0141/2338] update test fixtures to include buildConfigURI Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/mock_verifier.go | 8 +++++--- .../attestation/verify/attestation_integration_test.go | 2 +- pkg/cmd/attestation/verify/verify_test.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/attestation/verification/mock_verifier.go b/pkg/cmd/attestation/verification/mock_verifier.go index be828e66ff9..84a7ce3b856 100644 --- a/pkg/cmd/attestation/verification/mock_verifier.go +++ b/pkg/cmd/attestation/verification/mock_verifier.go @@ -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) } diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index caa02281fbf..630eb1dc589 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -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) diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 5e4f3350789..87ffa96f090 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -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) { From 4575692ebfbde0bfb9827e0fffea7d0751d9744f Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 8 Jan 2025 10:36:45 -0800 Subject: [PATCH 0142/2338] Remove private readBranchConfig method and remove parseBranchConfig from Client I think I went too far with my previous refactor and am backing out of it. Adding a private readBranchConfig method on the client wasn't providing any real additional value, so I've put it back into ReadBranchConfig. However, I think there is still value in having parseBranchConfig (formerly createBranchConfig) as a separate util function, as it both improves readability of ReadBranchConfig and makes parsing its purpose easier through the bespoke tests for it. --- git/client.go | 21 ++++------- git/client_test.go | 86 ++++++++++++---------------------------------- 2 files changed, 28 insertions(+), 79 deletions(-) diff --git a/git/client.go b/git/client.go index 20a489a2911..3cc53ca964f 100644 --- a/git/client.go +++ b/git/client.go @@ -381,35 +381,26 @@ func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte, // 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) { - var cfg BranchConfig - - branchConfigOutput, err := c.readGitBranchConfig(ctx, branch) - if err != nil { - return cfg, err - } - return c.createBranchConfig(branchConfigOutput), nil -} - -func (c *Client) readGitBranchConfig(ctx context.Context, branch string) ([]string, 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 []string{}, err + return BranchConfig{}, err } out, err := cmd.Output() if err != nil { - return []string{}, err + return BranchConfig{}, err } - return outputLines(out), nil + return parseBranchConfig(outputLines(out)), nil } -func (c *Client) createBranchConfig(gitBranchConfigOutput []string) BranchConfig { +func parseBranchConfig(configLines []string) BranchConfig { var cfg BranchConfig - for _, line := range gitBranchConfigOutput { + + for _, line := range configLines { parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { continue diff --git a/git/client_test.go b/git/client_test.go index 0930a6f7232..df9577946da 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -730,89 +730,48 @@ 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, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) - client := Client{ - GitPath: "path/to/git", - commandContext: cmdCtx, - } - branchConfig, _ := client.ReadBranchConfig(context.Background(), "trunk") - assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) - assert.Equal(t, tt.wantBranchConfig, branchConfig) - }) - } -} - -func Test_readGitBranchConfig(t *testing.T) { - tests := []struct { - name string - branch string - cmdExitStatus int - cmdStdout string - cmdStderr string - expectedOutput []string - expectedError *GitError - }{ { - name: "read branch config", - branch: "trunk", - cmdExitStatus: 0, - cmdStdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk", - expectedOutput: []string{ - "branch.trunk.remote origin", - "branch.trunk.merge refs/heads/trunk", - "branch.trunk.gh-merge-base trunk"}, - expectedError: nil, - }, - { - name: "command failure", - branch: "trunk", - cmdExitStatus: 1, - cmdStderr: "git error message", - expectedOutput: []string{}, - expectedError: &GitError{ - ExitCode: 1, - Stderr: "git error message", - }, + name: "command error", + cmdExitStatus: 1, + cmdStdout: "", + cmdStderr: "git error message", + branch: "trunk", + wantBranchConfig: BranchConfig{}, + wantError: &GitError{ExitCode: 1, Stderr: "git error message"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - commandString := fmt.Sprintf("path/to/git config --get-regexp ^branch\\.%s\\.(remote|merge|gh-merge-base)$", tt.branch) - cmdCtx := createMockedCommandContext(t, map[args]commandResult{ - args(commandString): { - ExitStatus: tt.cmdExitStatus, - Stdout: tt.cmdStdout, - Stderr: tt.cmdStderr, - }, - }) + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) client := Client{ GitPath: "path/to/git", commandContext: cmdCtx, } - - out, err := client.readGitBranchConfig(context.Background(), tt.branch) - assert.Equal(t, tt.expectedOutput, out) - if err != nil { - assert.Equal(t, tt.cmdStderr, err.(*GitError).Stderr) - assert.Equal(t, tt.cmdExitStatus, err.(*GitError).ExitCode) + 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.Equal(t, tt.wantError.ExitCode, err.(*GitError).ExitCode) + assert.Equal(t, tt.wantError.Stderr, err.(*GitError).Stderr) } }) } } -func Test_createBranchConfig(t *testing.T) { +func Test_parseBranchConfig(t *testing.T) { tests := []struct { name string gitBranchConfigOutput []string @@ -855,8 +814,7 @@ func Test_createBranchConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := Client{} - branchConfig := client.createBranchConfig(tt.gitBranchConfigOutput) + branchConfig := parseBranchConfig(tt.gitBranchConfigOutput) assert.Equal(t, tt.wantBranchConfig, branchConfig) }) } From fea46c00112cd39ddcc832c79d8c884fb5a045e5 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 8 Jan 2025 11:40:42 -0800 Subject: [PATCH 0143/2338] Change ReadBranchConfig error message in gh pr create --- pkg/cmd/pr/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 638d8eed4bc..b2229b976e5 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -683,7 +683,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { headBranchConfig, err := gitClient.ReadBranchConfig(context.Background(), headBranch) if err != nil { // We can still proceed without the branch config, so print to stderr but continue - fmt.Fprintf(opts.IO.ErrOut, "Error reading git branch config: %v\n", err) + fmt.Fprintf(opts.IO.ErrOut, "Unable to read git branch config: %v\n", err) } if isPushEnabled { // determine whether the head branch is already pushed to a remote From c2fdac272cd9769626369d7daf108dc4709e07e2 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 8 Jan 2025 13:27:38 -0700 Subject: [PATCH 0144/2338] update table column headers Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 73987d8a290..b0d82be3c8a 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -271,7 +271,7 @@ func runVerify(opts *Options) error { return err } - headers := []string{"signer repo", "signer workflow", "build repo", "build workflow"} + headers := []string{"signer repo", "signer workflow", "builder repo", "builder 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 From 527d1db29877fdf8665723cbe4c2668fb069dbe0 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 8 Jan 2025 14:24:20 -0800 Subject: [PATCH 0145/2338] Add missing test for RemoteURL in parseBranchConfig --- git/client_test.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/git/client_test.go b/git/client_test.go index df9577946da..5eb7f458529 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "os" "os/exec" "path/filepath" @@ -811,11 +812,32 @@ func Test_parseBranchConfig(t *testing.T) { MergeBase: "gh-merge-base", }, }, + { + name: "remote URL", + gitBranchConfigOutput: []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.gitBranchConfigOutput) - assert.Equal(t, tt.wantBranchConfig, branchConfig) + 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()) + } }) } } From ec9eaef048362eec8d620f8416b4b256d0d4e9d2 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 8 Jan 2025 14:27:41 -0800 Subject: [PATCH 0146/2338] Refactor prSelectorForCurrentBranch and tests Replace the git config argument in prSelectorForCurrentBranch with the branchConfig it was used to fetch. The tests needed to be refactored accordingly to support this change to the prSelectorForCurrentBranch API. In addition, I've moved the test to a table test format so I can expand the test coverage in the next commit. --- pkg/cmd/pr/status/status.go | 13 +++--- pkg/cmd/pr/status/status_test.go | 68 ++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 9245b1cdc11..499202b7e03 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -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,8 @@ func statusRun(opts *StatusOptions) error { } remotes, _ := opts.Remotes() - currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(opts.GitClient, baseRepo, currentBranch, remotes) + branchConfig, _ := opts.GitClient.ReadBranchConfig(ctx, currentBranch) + 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,14 +186,15 @@ func statusRun(opts *StatusOptions) error { return nil } -func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (int, string, error) { - 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, err := strconv.Atoi(m[1]) - return prNumber, prHeadRef, err + if err != nil { + return 0, "", err + } + return prNumber, prHeadRef, nil } var err error diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index e8e4875592d..6e2cf71f9cb 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -4,23 +4,23 @@ 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" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "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) { @@ -376,30 +376,48 @@ Requesting a code review from you } func Test_prSelectorForCurrentBranch(t *testing.T) { - rs, cleanup := run.Stub() - defer cleanup(t) - - 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 - `)) - - repo := ghrepo.NewWithHost("octocat", "playground", "github.com") - rem := context.Remotes{ - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: repo, + tests := []struct { + name string + branchConfig git.BranchConfig + baseRepo ghrepo.Interface + prHeadRef string + remotes context.Remotes + wantPrNumber int + wantSelector string + wantError error + }{ + { + 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: "Frederick888/playground.git", + }, + MergeRef: "refs/heads/main", + }, + baseRepo: ghrepo.NewWithHost("octocat", "playground", "github.com"), + prHeadRef: "Frederick888/main", + remotes: context.Remotes{ + &context.Remote{ + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.NewWithHost("octocat", "playground", "github.com"), + }, + }, + wantPrNumber: 0, + wantSelector: "Frederick888: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) + }) } } From 94b2d4ec3b0076cf33d4ec5d1ddbe9563299e988 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 8 Jan 2025 15:00:38 -0800 Subject: [PATCH 0147/2338] Added tests to cover prSelectorForCurrentBranch for confidence in refactor --- pkg/cmd/pr/status/status_test.go | 124 ++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 6e2cf71f9cb..9f307995778 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -386,6 +386,16 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { wantSelector string wantError error }{ + { + name: "The branch is configured to merge a special PR head ref", + branchConfig: git.BranchConfig{ + MergeRef: "refs/pull/666/head", + }, + prHeadRef: "Frederick888/main", + wantPrNumber: 666, + wantSelector: "Frederick888/main", + wantError: nil, + }, { name: "Branch merges from a remote specified by URL", branchConfig: git.BranchConfig{ @@ -395,18 +405,126 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { Host: "github.com", Path: "Frederick888/playground.git", }, + }, + baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + prHeadRef: "Frederick888/main", + remotes: context.Remotes{ + &context.Remote{ + Remote: &git.Remote{Name: "origin"}, + Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + }, + }, + wantPrNumber: 0, + wantSelector: "Frederick888/main", + wantError: nil, + }, + { + name: "Branch merges from a remote specified by name", + branchConfig: git.BranchConfig{ + RemoteName: "upstream", + }, + baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + prHeadRef: "Frederick888/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("Frederick888", "playground", "github.com"), + }, + }, + wantPrNumber: 0, + wantSelector: "Frederick888/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("octocat", "playground", "github.com"), + baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + prHeadRef: "Frederick888/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("Frederick888", "playground", "github.com"), prHeadRef: "Frederick888/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.NewWithHost("octocat", "playground", "github.com"), + Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"), + }, + &context.Remote{ + Remote: &git.Remote{Name: "upstream"}, + Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + }, + }, + wantPrNumber: 0, + wantSelector: "forkName:Frederick888/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("Frederick888", "playground", "github.com"), + prHeadRef: "Frederick888/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("Frederick888", "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("Frederick888", "playground", "github.com"), + prHeadRef: "Frederick888/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("Frederick888", "playground", "github.com"), }, }, wantPrNumber: 0, - wantSelector: "Frederick888:main", + wantSelector: "forkName:main", wantError: nil, }, } From 15ac56622212b5dac30d4b02dfbe9debf5d1e094 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 8 Jan 2025 15:20:17 -0800 Subject: [PATCH 0148/2338] Remove shadowed errors from prSelectorForCurrentBranch and cover with tests --- pkg/cmd/pr/status/status.go | 15 +++++++------ pkg/cmd/pr/status/status_test.go | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 499202b7e03..e97770ce450 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -197,18 +197,21 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I return prNumber, prHeadRef, nil } - var err error 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 { + return 0, prHeadRef, err } + 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 { + return 0, prHeadRef, err } + branchOwner = r.RepoOwner() } selector := prHeadRef @@ -222,7 +225,7 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I } } - return 0, selector, err + return 0, selector, nil } func totalApprovals(pr *api.PullRequest) int { diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 9f307995778..94505aaba64 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -2,6 +2,7 @@ package status import ( "bytes" + "fmt" "io" "net/http" "net/url" @@ -527,6 +528,41 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { wantSelector: "forkName:main", wantError: nil, }, + { + name: "Remote URL error", + branchConfig: git.BranchConfig{ + RemoteURL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "github.com", + Path: "/\\invalid?Path/", + }, + }, + prHeadRef: "Frederick888/main", + wantPrNumber: 0, + wantSelector: "Frederick888/main", + wantError: fmt.Errorf("invalid path: /\\invalid?Path/"), + }, + { + name: "Remote Name error", + branchConfig: git.BranchConfig{ + RemoteName: "nonexistentRemote", + }, + prHeadRef: "Frederick888/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("Frederick888", "playground", "github.com"), + }, + }, + wantPrNumber: 0, + wantSelector: "Frederick888/main", + wantError: fmt.Errorf("no matching remote found"), + }, } for _, tt := range tests { From 9c65a32ef86343786ec1fa4ef81de0fce412ff1b Mon Sep 17 00:00:00 2001 From: danochoa Date: Wed, 8 Jan 2025 17:56:02 -0600 Subject: [PATCH 0149/2338] gist delete prompt with gist description --- pkg/cmd/gist/delete/delete.go | 39 ++++++++++++++++-------------- pkg/cmd/gist/delete/delete_test.go | 29 ++++++++++++++++++++-- pkg/cmd/gist/edit/edit.go | 5 ++-- pkg/cmd/gist/shared/shared.go | 19 +++++++-------- pkg/cmd/gist/shared/shared_test.go | 30 +++++++++++------------ pkg/cmd/gist/view/view.go | 5 ++-- 6 files changed, 78 insertions(+), 49 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index a2b552a3a56..c39538d4afc 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -76,15 +76,6 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } 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 @@ -97,16 +88,28 @@ func deleteRun(opts *DeleteOptions) error { host, _ := cfg.Authentication().DefaultHost() - cs := opts.IO.ColorScheme() - if gistID == "" { - gistID, err = shared.PromptGists(opts.Prompter, client, host, cs) + 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 !opts.Confirmed { - confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete \"%s\" gist?", gistID), false) + confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete \"%s\" gist?", gist.Description), false) if err != nil { return err } @@ -116,9 +119,9 @@ func deleteRun(opts *DeleteOptions) error { } apiClient := api.NewClientFromHTTP(client) - if err := deleteGist(apiClient, host, gistID, opts); err != nil { + if err := deleteGist(apiClient, host, gist, opts); 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 %s: either the gist is not found or it is not owned by you", gist.Description) } return err } @@ -126,8 +129,8 @@ func deleteRun(opts *DeleteOptions) error { return nil } -func deleteGist(apiClient *api.Client, hostname string, gistID string, opts *DeleteOptions) error { - path := "gists/" + gistID +func deleteGist(apiClient *api.Client, hostname string, gist *shared.Gist, opts *DeleteOptions) error { + path := "gists/" + gist.ID err := apiClient.REST(hostname, "DELETE", path, nil, nil) if err != nil { var httpErr api.HTTPError @@ -141,7 +144,7 @@ func deleteGist(apiClient *api.Client, hostname string, gistID string, opts *Del fmt.Fprintf(opts.IO.Out, "%s Gist \"%s\" deleted\n", cs.SuccessIcon(), - gistID) + gist.Description) } return nil } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 237e3f76886..010f65d5e12 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -10,6 +10,7 @@ import ( "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" @@ -131,6 +132,8 @@ func Test_deleteRun(t *testing.T) { Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, @@ -159,6 +162,8 @@ func Test_deleteRun(t *testing.T) { Confirmed: true, }, httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, @@ -186,6 +191,24 @@ func Test_deleteRun(t *testing.T) { opts: &DeleteOptions{ Selector: "1234", }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) + }, + cancel: true, + wantErr: true, + wantStdout: "", + wantStderr: "", + }, + { + 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", Description: "1234"})) + }, cancel: true, wantErr: true, wantStdout: "", @@ -207,6 +230,8 @@ func Test_deleteRun(t *testing.T) { Selector: "1234", }, httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(404, "{}")) }, @@ -237,7 +262,7 @@ func Test_deleteRun(t *testing.T) { { "name": "1234", "files": [{ "name": "cool.txt" }], - "description": "", + "description": "1234", "updatedAt": "%s", "isPublic": true } @@ -246,7 +271,7 @@ func Test_deleteRun(t *testing.T) { )), ) - pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { + pm.RegisterSelect("Select a gist", []string{"cool.txt 1234 about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { return 0, nil }) } diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index d777e9581a0..1e9b6182544 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -108,15 +108,16 @@ func editRun(opts *EditOptions) error { if gistID == "" { cs := opts.IO.ColorScheme() if gistID == "" { - gistID, err = shared.PromptGists(opts.Prompter, client, host, cs) + 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 } } diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index ee6dcf1e3f5..fa528698f5b 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -202,21 +202,20 @@ 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 []string + var opts = make([]string, len(gists)) for i, gist := range gists { - gistIDs[i] = gist.ID description := "" gistName := "" @@ -234,15 +233,15 @@ func PromptGists(prompter prompter.Prompter, client *http.Client, host string, c 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(gistName), description, cs.Gray(gistTime)) + // opts = append(opts, opt) } result, err := prompter.Select("Select a gist", "", opts) if err != nil { - return "", err + return &Gist{}, err } - return gistIDs[result], nil + return &gists[result], nil } diff --git a/pkg/cmd/gist/shared/shared_test.go b/pkg/cmd/gist/shared/shared_test.go index 4d185ff7d51..15b14a9398f 100644 --- a/pkg/cmd/gist/shared/shared_test.go +++ b/pkg/cmd/gist/shared/shared_test.go @@ -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) }) } diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 2a19f595859..adfe77f6f32 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -89,15 +89,16 @@ func viewRun(opts *ViewOptions) error { cs := opts.IO.ColorScheme() if gistID == "" { - gistID, err = shared.PromptGists(opts.Prompter, client, hostname, cs) + 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 { From dccbc99fdfd94b4e179e8367e7e2dc3f009f5b99 Mon Sep 17 00:00:00 2001 From: danochoa Date: Wed, 8 Jan 2025 20:47:50 -0600 Subject: [PATCH 0150/2338] fix gist description in tests --- pkg/cmd/gist/delete/delete.go | 2 +- pkg/cmd/gist/delete/delete_test.go | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index c39538d4afc..9a77fe1f235 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -121,7 +121,7 @@ func deleteRun(opts *DeleteOptions) error { apiClient := api.NewClientFromHTTP(client) if err := deleteGist(apiClient, host, gist, opts); 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", gist.Description) + return fmt.Errorf("unable to delete gist \"%s\": either the gist is not found or it is not owned by you", gist.Description) } return err } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 010f65d5e12..d4ec36a0c28 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -133,12 +133,12 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "✓ Gist \"1234\" deleted\n", + wantStdout: "✓ Gist \"my cool text\" deleted\n", wantStderr: "", }, { @@ -152,7 +152,7 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "✓ Gist \"1234\" deleted\n", + wantStdout: "✓ Gist \"my cool text\" deleted\n", wantStderr: "", }, { @@ -163,12 +163,12 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "✓ Gist \"1234\" deleted\n", + wantStdout: "✓ Gist \"my cool text\" deleted\n", wantStderr: "", }, { @@ -183,7 +183,7 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "✓ Gist \"1234\" deleted\n", + wantStdout: "✓ Gist \"my cool text\" deleted\n", wantStderr: "", }, { @@ -193,7 +193,7 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) }, cancel: true, wantErr: true, @@ -207,7 +207,7 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) }, cancel: true, wantErr: true, @@ -231,20 +231,20 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "1234"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(404, "{}")) }, wantErr: true, wantStdout: "", - wantStderr: "unable to delete gist 1234: either the gist is not found or it is not owned by you", + wantStderr: "unable to delete gist \"my cool text\": either the gist is not found or it is not owned by you", }, } for _, tt := range tests { pm := prompter.NewMockPrompter(t) if !tt.opts.Confirmed { - pm.RegisterConfirm("Delete \"1234\" gist?", func(_ string, _ bool) (bool, error) { + pm.RegisterConfirm("Delete \"my cool text\" gist?", func(_ string, _ bool) (bool, error) { return !tt.cancel, nil }) } @@ -262,7 +262,7 @@ func Test_deleteRun(t *testing.T) { { "name": "1234", "files": [{ "name": "cool.txt" }], - "description": "1234", + "description": "my cool text", "updatedAt": "%s", "isPublic": true } @@ -271,7 +271,7 @@ func Test_deleteRun(t *testing.T) { )), ) - pm.RegisterSelect("Select a gist", []string{"cool.txt 1234 about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { + pm.RegisterSelect("Select a gist", []string{"cool.txt my cool text about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { return 0, nil }) } From 6bbd70353b03975bc230759fadf9d58a418e226d Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Thu, 9 Jan 2025 09:09:42 -0500 Subject: [PATCH 0151/2338] Comment exported functions --- internal/update/update.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/update/update.go b/internal/update/update.go index 30a4b41de25..1afabaf21d0 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -33,6 +33,7 @@ type StateEntry struct { LatestRelease ReleaseInfo `yaml:"latest_release"` } +// ShouldCheckForUpdate decides whether we check for updates for the GitHub CLI and/or GitHub CLI extensions based on user preferences and current execution context. func ShouldCheckForUpdate() bool { if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { return false @@ -43,6 +44,7 @@ func ShouldCheckForUpdate() bool { return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) } +// CheckForExtensionUpdate checks whether an update exists for a specific extension based on extension type and recency of last check within past 24 hours. func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, now time.Time) (*ReleaseInfo, error) { // local extensions cannot have updates, so avoid work that ultimately returns nothing. if ext.IsLocal() { @@ -72,7 +74,7 @@ func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte return nil, nil } -// CheckForUpdate checks whether this software has had a newer release on GitHub +// CheckForUpdate checks whether an update exists for the GitHub CLI based on recency of last check within past 24 hours. func CheckForUpdate(ctx context.Context, client *http.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) { stateEntry, _ := getStateEntry(stateFilePath) if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { @@ -164,11 +166,13 @@ func versionGreaterThan(v, w string) bool { return ve == nil && we == nil && vv.GreaterThan(vw) } +// IsTerminal determines if a file descriptor is an interactive terminal / TTY. func IsTerminal(f *os.File) bool { return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd()) } -// based on https://github.com/watson/ci-info/blob/HEAD/index.js +// IsCI determines if the current execution context is within a known CI/CD system. +// This is based on https://github.com/watson/ci-info/blob/HEAD/index.js. func IsCI() bool { return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity From f4f8840c3f53587f398240571c5affebc149d8c1 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Thu, 9 Jan 2025 12:07:15 -0300 Subject: [PATCH 0152/2338] #10042: Add error messages for 'gh gist view/edit' prompts when no TTY is detected --- pkg/cmd/gist/edit/edit.go | 3 +++ pkg/cmd/gist/view/view.go | 6 +++++- pkg/cmd/gist/view/view_test.go | 17 +++++------------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index d777e9581a0..ced995f6329 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -108,6 +108,9 @@ func editRun(opts *EditOptions) error { if gistID == "" { cs := opts.IO.ColorScheme() if gistID == "" { + if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("gist ID or URL required when not running interactively") + } gistID, err = shared.PromptGists(opts.Prompter, client, host, cs) if err != nil { return err diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index a6f4d528a12..25feccdc123 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -55,7 +55,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman } if !opts.IO.IsStdoutTTY() { - return cmdutil.FlagErrorf("run or gist ID required when not running interactively\n\nUsage: gh gist view [ | ] [flags]\n\nFlags:\n -f, --filename string Display a single file from the gist\n --files List file names from the gist\n -r, --raw Print raw instead of rendered gist contents\n -w, --web Open gist in the browser\n") + opts.Raw = true } if runF != nil { @@ -89,6 +89,10 @@ func viewRun(opts *ViewOptions) error { cs := opts.IO.ColorScheme() if gistID == "" { + if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("gist ID or URL required when not running interactively") + } + gistID, err = shared.PromptGists(opts.Prompter, client, hostname, cs) if err != nil { return err diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 2e80ee082cb..e52a3b9cb3c 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -20,11 +20,10 @@ import ( func TestNewCmdView(t *testing.T) { tests := []struct { - name string - cli string - wants ViewOptions - tty bool - wantsErr bool + name string + cli string + wants ViewOptions + tty bool }{ { name: "tty no arguments", @@ -45,7 +44,6 @@ func TestNewCmdView(t *testing.T) { Selector: "123", ListFiles: false, }, - wantsErr: true, }, { name: "filename passed", @@ -98,11 +96,6 @@ func TestNewCmdView(t *testing.T) { return nil }) - _, err = cmd.ExecuteC() - if tt.wantsErr { - assert.Error(t, err) - return - } cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{}) @@ -166,7 +159,7 @@ func Test_viewRun(t *testing.T) { }, }, }, - wantOut: "test interactive mode\n", + wantErr: true, }, { name: "filename selected", From 954d4d5dbc6254f2e868ccf97fa5c6aae1cda275 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Thu, 9 Jan 2025 11:08:18 -0500 Subject: [PATCH 0153/2338] Correct help documentation around template use in `gh issue create` Relates #10202 While we figure out how to handle consistent experience using templates for creating issues and PRs, let's correct the help usage for issue template flag use as this is the issue template name, not filename. --- pkg/cmd/issue/create/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index f51cde4c908..2e3e0de519a 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -72,7 +72,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co $ gh issue create --assignee monalisa,hubot $ gh issue create --assignee "@me" $ gh issue create --project "Roadmap" - $ gh issue create --template "bug_report.md" + $ gh issue create --template "Bug Report" `), Args: cmdutil.NoArgsQuoteReminder, Aliases: []string{"new"}, @@ -129,7 +129,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `title`") cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`") cmd.Flags().StringVar(&opts.RecoverFile, "recover", "", "Recover input from a failed run of create") - cmd.Flags().StringVarP(&opts.Template, "template", "T", "", "Template `file` to use as starting body text") + cmd.Flags().StringVarP(&opts.Template, "template", "T", "", "Template `name` to use as starting body text") return cmd } From 4c2ba5681d4777dc364bad989eda6a68189ca6a5 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Thu, 9 Jan 2025 13:14:27 -0300 Subject: [PATCH 0154/2338] #10042 removed unnecessary field --- pkg/cmd/gist/view/view_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index e52a3b9cb3c..37dda42d118 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -37,7 +37,6 @@ func TestNewCmdView(t *testing.T) { }, { name: "nontty no arguments", - tty: false, cli: "123", wants: ViewOptions{ Raw: true, From d4f7576e8baaccfdd3f7e9c4418151294d67733c Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 09:01:14 -0800 Subject: [PATCH 0155/2338] Add test for empty BranchConfig in prSelectorForCurrentBranch --- pkg/cmd/pr/status/status.go | 5 +++-- pkg/cmd/pr/status/status_test.go | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index e97770ce450..c15c0f7fd17 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -214,8 +214,8 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I branchOwner = r.RepoOwner() } - selector := prHeadRef if branchOwner != "" { + selector := prHeadRef if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } @@ -223,9 +223,10 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I if !strings.EqualFold(branchOwner, baseRepo.RepoOwner()) { selector = fmt.Sprintf("%s:%s", branchOwner, selector) } + return 0, selector, nil } - return 0, selector, nil + return 0, prHeadRef, nil } func totalApprovals(pr *api.PullRequest) int { diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 94505aaba64..43131215506 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -387,6 +387,14 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { wantSelector string wantError error }{ + { + name: "Empty branch config", + branchConfig: git.BranchConfig{}, + prHeadRef: "Frederick888/main", + wantPrNumber: 0, + wantSelector: "Frederick888/main", + wantError: nil, + }, { name: "The branch is configured to merge a special PR head ref", branchConfig: git.BranchConfig{ From e1423cdbbf36a0a49385eea2312aaaf8dc70dae4 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 10:08:47 -0800 Subject: [PATCH 0156/2338] Refine error handling of ReadBranchConfig cmd.Output() will return an error when the git command ran successfully but had no output. To handle this, we can check Stderr, as we expect it to be populated for any ExitErrors or otherwise when there is a command failure. This allows for propagation of this error handling up the call chain, so we are now returning errors if the call to git fails instead of just handing off an empty BranchConfig and suppressing the errors. Additionally, I've removed some more naked returns that I found in pkg/cmd/pr/create.go createRun --- git/client.go | 8 ++++++- git/client_test.go | 40 ++++++++++++++++++++++------------ pkg/cmd/pr/create/create.go | 43 ++++++++++++++++++------------------- pkg/cmd/pr/status/status.go | 5 ++++- 4 files changed, 58 insertions(+), 38 deletions(-) diff --git a/git/client.go b/git/client.go index 3cc53ca964f..16e8f32b53e 100644 --- a/git/client.go +++ b/git/client.go @@ -391,7 +391,13 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon out, err := cmd.Output() if err != nil { - return BranchConfig{}, err + // This will error if no matches are found but the git command still ran successfully. We only + // want to return an error if the command failed to run, usually and ExitError, which will be + // indicated by output on Stderr. + if err.(*GitError).Stderr != "" { + return BranchConfig{}, err + } + return BranchConfig{}, nil } return parseBranchConfig(outputLines(out)), nil diff --git a/git/client_test.go b/git/client_test.go index 5eb7f458529..4b721656e5e 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -744,7 +744,7 @@ func TestClientReadBranchConfig(t *testing.T) { wantError: nil, }, { - name: "command error", + name: "output error", cmdExitStatus: 1, cmdStdout: "", cmdStderr: "git error message", @@ -752,6 +752,15 @@ func TestClientReadBranchConfig(t *testing.T) { wantBranchConfig: BranchConfig{}, wantError: &GitError{ExitCode: 1, Stderr: "git error message"}, }, + { + name: "git config runs successfully but returns no output", + cmdExitStatus: 1, + cmdStdout: "", + cmdStderr: "", + branch: "trunk", + wantBranchConfig: BranchConfig{}, + wantError: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -764,7 +773,10 @@ func TestClientReadBranchConfig(t *testing.T) { 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 { + if err != nil { + if tt.wantError == nil { + t.Fatalf("expected no error but got %v", err) + } assert.Equal(t, tt.wantError.ExitCode, err.(*GitError).ExitCode) assert.Equal(t, tt.wantError.Stderr, err.(*GitError).Stderr) } @@ -774,34 +786,34 @@ func TestClientReadBranchConfig(t *testing.T) { func Test_parseBranchConfig(t *testing.T) { tests := []struct { - name string - gitBranchConfigOutput []string - wantBranchConfig BranchConfig + name string + configLines []string + wantBranchConfig BranchConfig }{ { - name: "remote branch", - gitBranchConfigOutput: []string{"branch.trunk.remote origin"}, + name: "remote branch", + configLines: []string{"branch.trunk.remote origin"}, wantBranchConfig: BranchConfig{ RemoteName: "origin", }, }, { - name: "merge ref", - gitBranchConfigOutput: []string{"branch.trunk.merge refs/heads/trunk"}, + name: "merge ref", + configLines: []string{"branch.trunk.merge refs/heads/trunk"}, wantBranchConfig: BranchConfig{ MergeRef: "refs/heads/trunk", }, }, { - name: "merge base", - gitBranchConfigOutput: []string{"branch.trunk.gh-merge-base gh-merge-base"}, + 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", - gitBranchConfigOutput: []string{ + configLines: []string{ "branch.trunk.remote origin", "branch.trunk.merge refs/heads/trunk", "branch.trunk.gh-merge-base gh-merge-base", @@ -814,7 +826,7 @@ func Test_parseBranchConfig(t *testing.T) { }, { name: "remote URL", - gitBranchConfigOutput: []string{ + configLines: []string{ "branch.Frederick888/main.remote git@github.com:Frederick888/playground.git", "branch.Frederick888/main.merge refs/heads/main", }, @@ -831,7 +843,7 @@ func Test_parseBranchConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - branchConfig := parseBranchConfig(tt.gitBranchConfigOutput) + 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) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index b2229b976e5..124bd4b0724 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -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)^`) @@ -682,8 +682,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { headBranchConfig, err := gitClient.ReadBranchConfig(context.Background(), headBranch) if err != nil { - // We can still proceed without the branch config, so print to stderr but continue - fmt.Fprintf(opts.IO.ErrOut, "Unable to read git branch config: %v\n", err) + return nil, err } if isPushEnabled { // determine whether the head branch is already pushed to a remote diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index c15c0f7fd17..d22ae4e2f3c 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -94,7 +94,10 @@ func statusRun(opts *StatusOptions) error { } remotes, _ := opts.Remotes() - branchConfig, _ := opts.GitClient.ReadBranchConfig(ctx, currentBranch) + 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) From 48d45d0b9db6e5be0ce8f7fe50a4cac9b01e04cc Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 11:45:45 -0800 Subject: [PATCH 0157/2338] Refactor error handling in ReadBranchConfig to avoid panic --- git/client.go | 12 +++++++----- git/client_test.go | 12 +++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/git/client.go b/git/client.go index 16e8f32b53e..820a9cdc7a4 100644 --- a/git/client.go +++ b/git/client.go @@ -391,11 +391,13 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon out, err := cmd.Output() if err != nil { - // This will error if no matches are found but the git command still ran successfully. We only - // want to return an error if the command failed to run, usually and ExitError, which will be - // indicated by output on Stderr. - if err.(*GitError).Stderr != "" { - return BranchConfig{}, err + // This is the error we expect if the git command does not run successfully. + // Note: err is non-nil if the command is successful but has no output + var gitError *GitError + if errors.As(err, &gitError) { + if gitError.Stderr != "" { + return BranchConfig{}, err + } } return BranchConfig{}, nil } diff --git a/git/client_test.go b/git/client_test.go index 4b721656e5e..94d1c3969a9 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -750,7 +750,7 @@ func TestClientReadBranchConfig(t *testing.T) { cmdStderr: "git error message", branch: "trunk", wantBranchConfig: BranchConfig{}, - wantError: &GitError{ExitCode: 1, Stderr: "git error message"}, + wantError: &GitError{}, }, { name: "git config runs successfully but returns no output", @@ -773,12 +773,10 @@ func TestClientReadBranchConfig(t *testing.T) { 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 err != nil { - if tt.wantError == nil { - t.Fatalf("expected no error but got %v", err) - } - assert.Equal(t, tt.wantError.ExitCode, err.(*GitError).ExitCode) - assert.Equal(t, tt.wantError.Stderr, err.(*GitError).Stderr) + if tt.wantError != nil { + assert.ErrorAs(t, err, &tt.wantError) + } else { + assert.NoError(t, err) } }) } From e4320ccc4bf39dd3cf0e8eba2a6872b6d42eccca Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 11:58:33 -0800 Subject: [PATCH 0158/2338] Directly stub headBranchConfig in Test_tryDetermineTrackingRef --- pkg/cmd/pr/create/create_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 66fe1c39a98..b8f5daf6638 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1,7 +1,6 @@ package create import ( - ctx "context" "encoding/json" "errors" "fmt" @@ -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) From e4f0b791731e3093cc998d773e083a22f17c223e Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 12:46:48 -0800 Subject: [PATCH 0159/2338] Surface and handle error from ReadBranchConfig in parseCurrentBranch I've only added the one test for parseCurrentBranch because the function appears to be largely exercised by TestFind. There's definitely an opportunity for a bigger refactor of the tests, here, but I want to avoid scope creep as I propagate the ReadBranchConfig api changes throughout the codebase --- pkg/cmd/pr/shared/finder.go | 12 ++-- pkg/cmd/pr/shared/finder_test.go | 113 +++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 3bc197b75df..3f036c0cdd5 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -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,9 +58,8 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { remotesFn: factory.Remotes, httpClient: factory.HttpClient, progress: factory.IOStreams, - branchConfig: func(s string) git.BranchConfig { - branchConfig, _ := factory.GitClient.ReadBranchConfig(context.Background(), s) - return branchConfig + branchConfig: func(s string) (git.BranchConfig, error) { + return factory.GitClient.ReadBranchConfig(context.Background(), s) }, } } @@ -239,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 { diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 258ef01e829..dd96e684a66 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -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 + } +} From 100f9b473f8e7c53cb640c41fd00eb74b9e8926c Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 12:57:23 -0800 Subject: [PATCH 0160/2338] Change pr number in test --- pkg/cmd/pr/status/status_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 43131215506..5ccc80eabe0 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -398,10 +398,10 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "The branch is configured to merge a special PR head ref", branchConfig: git.BranchConfig{ - MergeRef: "refs/pull/666/head", + MergeRef: "refs/pull/42/head", }, prHeadRef: "Frederick888/main", - wantPrNumber: 666, + wantPrNumber: 42, wantSelector: "Frederick888/main", wantError: nil, }, From 3d2748789e18fbd0f910d73d2c4c78712efbb1f6 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 9 Jan 2025 12:58:31 -0800 Subject: [PATCH 0161/2338] Rename test user in tests --- pkg/cmd/pr/status/status_test.go | 60 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 5ccc80eabe0..eb40912b416 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -390,9 +390,9 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "Empty branch config", branchConfig: git.BranchConfig{}, - prHeadRef: "Frederick888/main", + prHeadRef: "monalisa/main", wantPrNumber: 0, - wantSelector: "Frederick888/main", + wantSelector: "monalisa/main", wantError: nil, }, { @@ -400,9 +400,9 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { branchConfig: git.BranchConfig{ MergeRef: "refs/pull/42/head", }, - prHeadRef: "Frederick888/main", + prHeadRef: "monalisa/main", wantPrNumber: 42, - wantSelector: "Frederick888/main", + wantSelector: "monalisa/main", wantError: nil, }, { @@ -412,19 +412,19 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { Scheme: "ssh", User: url.User("git"), Host: "github.com", - Path: "Frederick888/playground.git", + Path: "monalisa/playground.git", }, }, - baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), - prHeadRef: "Frederick888/main", + baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), }, }, wantPrNumber: 0, - wantSelector: "Frederick888/main", + wantSelector: "monalisa/main", wantError: nil, }, { @@ -432,8 +432,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { branchConfig: git.BranchConfig{ RemoteName: "upstream", }, - baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), - prHeadRef: "Frederick888/main", + baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, @@ -441,11 +441,11 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, - Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), }, }, wantPrNumber: 0, - wantSelector: "Frederick888/main", + wantSelector: "monalisa/main", wantError: nil, }, { @@ -459,8 +459,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, MergeRef: "refs/heads/main", }, - baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), - prHeadRef: "Frederick888/main", + baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, @@ -476,8 +476,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { branchConfig: git.BranchConfig{ RemoteName: "origin", }, - baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), - prHeadRef: "Frederick888/main", + baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, @@ -485,11 +485,11 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, - Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), }, }, wantPrNumber: 0, - wantSelector: "forkName:Frederick888/main", + wantSelector: "forkName:monalisa/main", wantError: nil, }, { @@ -498,8 +498,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { RemoteName: "upstream", MergeRef: "refs/heads/main", }, - baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), - prHeadRef: "Frederick888/main", + baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, @@ -507,7 +507,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, - Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), }, }, wantPrNumber: 0, @@ -520,8 +520,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { RemoteName: "origin", MergeRef: "refs/heads/main", }, - baseRepo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), - prHeadRef: "Frederick888/main", + baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, @@ -529,7 +529,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, - Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), }, }, wantPrNumber: 0, @@ -546,9 +546,9 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { Path: "/\\invalid?Path/", }, }, - prHeadRef: "Frederick888/main", + prHeadRef: "monalisa/main", wantPrNumber: 0, - wantSelector: "Frederick888/main", + wantSelector: "monalisa/main", wantError: fmt.Errorf("invalid path: /\\invalid?Path/"), }, { @@ -556,7 +556,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { branchConfig: git.BranchConfig{ RemoteName: "nonexistentRemote", }, - prHeadRef: "Frederick888/main", + prHeadRef: "monalisa/main", remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, @@ -564,11 +564,11 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, - Repo: ghrepo.NewWithHost("Frederick888", "playground", "github.com"), + Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"), }, }, wantPrNumber: 0, - wantSelector: "Frederick888/main", + wantSelector: "monalisa/main", wantError: fmt.Errorf("no matching remote found"), }, } From 7f71681ace24411034ee18b44fc9288e4c14a06b Mon Sep 17 00:00:00 2001 From: danochoa Date: Fri, 10 Jan 2025 01:07:11 -0600 Subject: [PATCH 0162/2338] gist delete - use in confirmation and success info --- pkg/cmd/gist/delete/delete.go | 8 +++---- pkg/cmd/gist/delete/delete_test.go | 22 +++++++++--------- pkg/cmd/gist/shared/shared.go | 36 +++++++++++++++--------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index 9a77fe1f235..af070b10ef5 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -109,7 +109,7 @@ func deleteRun(opts *DeleteOptions) error { } if !opts.Confirmed { - confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete \"%s\" gist?", gist.Description), false) + confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete %q gist?", gist.FilenameDescription()), false) if err != nil { return err } @@ -121,7 +121,7 @@ func deleteRun(opts *DeleteOptions) error { apiClient := api.NewClientFromHTTP(client) if err := deleteGist(apiClient, host, gist, opts); 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", gist.Description) + return fmt.Errorf("unable to delete gist %q: either the gist is not found or it is not owned by you", gist.FilenameDescription()) } return err } @@ -142,9 +142,9 @@ func deleteGist(apiClient *api.Client, hostname string, gist *shared.Gist, opts if opts.IO.IsStdoutTTY() { cs := opts.IO.ColorScheme() fmt.Fprintf(opts.IO.Out, - "%s Gist \"%s\" deleted\n", + "%s Gist %q deleted\n", cs.SuccessIcon(), - gist.Description) + gist.FilenameDescription()) } return nil } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index d4ec36a0c28..729f81527fb 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -133,12 +133,12 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "✓ Gist \"my cool text\" deleted\n", + wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", wantStderr: "", }, { @@ -152,7 +152,7 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "✓ Gist \"my cool text\" deleted\n", + wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", wantStderr: "", }, { @@ -163,12 +163,12 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "✓ Gist \"my cool text\" deleted\n", + wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", wantStderr: "", }, { @@ -183,7 +183,7 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "✓ Gist \"my cool text\" deleted\n", + wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", wantStderr: "", }, { @@ -193,7 +193,7 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) }, cancel: true, wantErr: true, @@ -207,7 +207,7 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) }, cancel: true, wantErr: true, @@ -231,20 +231,20 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text"})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(404, "{}")) }, wantErr: true, wantStdout: "", - wantStderr: "unable to delete gist \"my cool text\": either the gist is not found or it is not owned by you", + wantStderr: "unable to delete gist \"cool.txt my cool text\": either the gist is not found or it is not owned by you", }, } for _, tt := range tests { pm := prompter.NewMockPrompter(t) if !tt.opts.Confirmed { - pm.RegisterConfirm("Delete \"my cool text\" gist?", func(_ string, _ bool) (bool, error) { + pm.RegisterConfirm("Delete \"cool.txt my cool text\" gist?", func(_ string, _ bool) (bool, error) { return !tt.cancel, nil }) } diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index fa528698f5b..085eec0abbb 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -39,6 +39,23 @@ 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)) +} + +func (g Gist) FilenameDescription() string { + return g.Filename() + " " + g.TruncDescription() +} + var NotFoundErr = errors.New("not found") func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) { @@ -212,29 +229,12 @@ func PromptGists(prompter prompter.Prompter, client *http.Client, host string, c return &Gist{}, nil } - // var opts []string var opts = make([]string, len(gists)) for i, gist := range gists { - 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)) - opts[i] = 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) From ae36160ea571e702cc1e8b65da2c1ecac1ebe204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:39:20 +0000 Subject: [PATCH 0163/2338] Bump github.com/sigstore/protobuf-specs from 0.3.2 to 0.3.3 Bumps [github.com/sigstore/protobuf-specs](https://github.com/sigstore/protobuf-specs) from 0.3.2 to 0.3.3. - [Release notes](https://github.com/sigstore/protobuf-specs/releases) - [Changelog](https://github.com/sigstore/protobuf-specs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sigstore/protobuf-specs/compare/v0.3.2...v0.3.3) --- updated-dependencies: - dependency-name: github.com/sigstore/protobuf-specs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f2f1ce81239..14277d49e20 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ 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 @@ -50,7 +50,7 @@ require ( golang.org/x/term v0.27.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.2 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 1b48f0c2744..af84c7e418e 100644 --- a/go.sum +++ b/go.sum @@ -392,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= @@ -548,8 +548,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.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/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= From 567624b5506355e466ac4ff4a7d094f62f48e2fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:39:26 +0000 Subject: [PATCH 0164/2338] Bump github.com/mattn/go-colorable from 0.1.13 to 0.1.14 Bumps [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) from 0.1.13 to 0.1.14. - [Commits](https://github.com/mattn/go-colorable/compare/v0.1.13...v0.1.14) --- updated-dependencies: - dependency-name: github.com/mattn/go-colorable dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index f2f1ce81239..cd7a8203633 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/in-toto/attestation v1.1.0 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/mattn/go-colorable v0.1.13 + github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/microsoft/dev-tunnels v0.0.25 @@ -160,7 +160,7 @@ require ( 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/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/tools v0.26.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 diff --git a/go.sum b/go.sum index 1b48f0c2744..0ec9bcd6a79 100644 --- a/go.sum +++ b/go.sum @@ -303,10 +303,9 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -514,11 +513,10 @@ 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-20220811171246-fbc7d0a398ab/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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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= From 889c4c75f380b7a1e801638f401bc0558d4021d7 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 10 Jan 2025 16:03:59 +0100 Subject: [PATCH 0165/2338] Collapse dryrun checks in ext bin upgrade --- pkg/cmd/extension/manager.go | 42 ++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 44d90b38633..5881269cf07 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -328,32 +328,30 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { errorMessageInRed, platform, issueCreateCommand) } - name := repo.RepoName() - targetDir := filepath.Join(m.installDir(), name) + if m.dryRunMode { + return nil + } - // TODO clean this up if function errs? - if !m.dryRunMode { - if err := m.cleanExtensionUpdateDir(name); err != nil { - return err - } + name := repo.RepoName() + if err := m.cleanExtensionUpdateDir(name); err != nil { + return err + } - if err = os.MkdirAll(targetDir, 0755); err != nil { - return fmt.Errorf("failed to create installation directory: %w", err) - } + targetDir := filepath.Join(m.installDir(), name) + if err = os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create installation directory: %w", err) } binPath := filepath.Join(targetDir, name) binPath += ext - if !m.dryRunMode { - err = downloadAsset(m.client, *asset, binPath) - if err != nil { - return fmt.Errorf("failed to download asset %s: %w", asset.Name, err) - } - if trueARMBinary { - if err := codesignBinary(binPath); err != nil { - return fmt.Errorf("failed to codesign downloaded binary: %w", err) - } + err = downloadAsset(m.client, *asset, binPath) + if err != nil { + return fmt.Errorf("failed to download asset %s: %w", asset.Name, err) + } + if trueARMBinary { + if err := codesignBinary(binPath); err != nil { + return fmt.Errorf("failed to codesign downloaded binary: %w", err) } } @@ -371,10 +369,8 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { return fmt.Errorf("failed to serialize manifest: %w", err) } - if !m.dryRunMode { - if err := writeManifest(targetDir, manifestName, bs); err != nil { - return err - } + if err := writeManifest(targetDir, manifestName, bs); err != nil { + return err } return nil From 1899b835092f6467426734ea72469038f1f3e7db Mon Sep 17 00:00:00 2001 From: nobe4 Date: Fri, 10 Jan 2025 16:28:50 +0100 Subject: [PATCH 0166/2338] fix: padded display Apply code suggestion from @andyfeller from: - https://github.com/cli/cli/pull/10194#discussion_r1909761552 - https://github.com/cli/cli/pull/10194#discussion_r1909764278 --- pkg/cmd/extension/manager.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 49c448b7e45..32f0be26cd3 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "runtime" + "slices" "strings" "sync" @@ -469,18 +470,23 @@ func (m *Manager) Upgrade(name string, force bool) error { } func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error { - var longestExtName = 0 - for _, ext := range exts { - l := len(ext.Name()) - if len(ext.Name()) > longestExtName { - longestExtName = l + var longestExt = slices.MaxFunc(exts, func(a, b *Extension) int { + la, lb := len(a.Name()), len(b.Name()) + if la == lb { + return 0 } - } - format := fmt.Sprintf("[%%%ds]: ", longestExtName) + + if la < lb { + return -1 + } + + return 1 + }) + var longestExtName = len(longestExt.Name()) var failed bool for _, f := range exts { - fmt.Fprintf(m.io.Out, format, f.Name()) + fmt.Fprintf(m.io.Out, "[%*s] ", longestExtName, f.Name()) currentVersion := displayExtensionVersion(f, f.CurrentVersion()) err := m.upgradeExtension(f, force) if err != nil { From 69ab0c9c85c42f95900ec64951ef8351c6c45fdf Mon Sep 17 00:00:00 2001 From: nobe4 Date: Fri, 10 Jan 2025 16:31:13 +0100 Subject: [PATCH 0167/2338] fix: actually read how MaxFunc work and simplify the code --- pkg/cmd/extension/manager.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 32f0be26cd3..c82d6e85663 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -471,16 +471,7 @@ func (m *Manager) Upgrade(name string, force bool) error { func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error { var longestExt = slices.MaxFunc(exts, func(a, b *Extension) int { - la, lb := len(a.Name()), len(b.Name()) - if la == lb { - return 0 - } - - if la < lb { - return -1 - } - - return 1 + return len(a.Name()) - len(b.Name()) }) var longestExtName = len(longestExt.Name()) From 69b17272d60ce36d785e416f40bace47b8fe8b05 Mon Sep 17 00:00:00 2001 From: nobe4 Date: Fri, 10 Jan 2025 17:17:14 +0100 Subject: [PATCH 0168/2338] fix: add back colon that I removed --- pkg/cmd/extension/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index c82d6e85663..7f4bcfd70ac 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -477,7 +477,7 @@ func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error { var failed bool for _, f := range exts { - fmt.Fprintf(m.io.Out, "[%*s] ", longestExtName, f.Name()) + fmt.Fprintf(m.io.Out, "[%*s]: ", longestExtName, f.Name()) currentVersion := displayExtensionVersion(f, f.CurrentVersion()) err := m.upgradeExtension(f, force) if err != nil { From 322a43feff32c0071705c8b3fd75091ffb8ebc2f Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 10 Jan 2025 09:59:21 -0800 Subject: [PATCH 0169/2338] Change error handling on ReadBranchConfig to respect git Exit Codes --- git/client.go | 8 +++----- git/client_test.go | 14 +++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/git/client.go b/git/client.go index 820a9cdc7a4..19688ca517b 100644 --- a/git/client.go +++ b/git/client.go @@ -392,12 +392,10 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon out, err := cmd.Output() if err != nil { // This is the error we expect if the git command does not run successfully. - // Note: err is non-nil if the command is successful but has no output + // If the ExitCode is 1, then we just didn't find any config for the branch. var gitError *GitError - if errors.As(err, &gitError) { - if gitError.Stderr != "" { - return BranchConfig{}, err - } + if ok := errors.As(err, &gitError); ok && gitError.ExitCode != 1 { + return BranchConfig{}, err } return BranchConfig{}, nil } diff --git a/git/client_test.go b/git/client_test.go index 94d1c3969a9..fe3047db99a 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -744,22 +744,22 @@ func TestClientReadBranchConfig(t *testing.T) { wantError: nil, }, { - name: "output error", + name: "git config runs successfully but returns no output (Exit Code 1)", cmdExitStatus: 1, cmdStdout: "", - cmdStderr: "git error message", + cmdStderr: "", branch: "trunk", wantBranchConfig: BranchConfig{}, - wantError: &GitError{}, + wantError: nil, }, { - name: "git config runs successfully but returns no output", - cmdExitStatus: 1, + name: "output error (Exit Code > 1)", + cmdExitStatus: 2, cmdStdout: "", - cmdStderr: "", + cmdStderr: "git error message", branch: "trunk", wantBranchConfig: BranchConfig{}, - wantError: nil, + wantError: &GitError{}, }, } for _, tt := range tests { From 74cbd50d3ff44cef698d8b77056122c257fb32b9 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 10 Jan 2025 10:51:55 -0800 Subject: [PATCH 0170/2338] Restore old error functionality of prSelectorForCurrentBranch Before this refactor, the errors emitted by ghrepo.FromURL and rem.FindName() were suppressed. It isn't clear whether this was intentional or not, but we've made the decision here to maintain the original error behavior while still refactoring the return values for more clarity. I've left a comment at each error handling block to explain this decision. Additionally, I've added the necessary git command stubs to the other tests in status_test.go so that the tests are now passing. --- pkg/cmd/pr/status/status.go | 10 ++++- pkg/cmd/pr/status/status_test.go | 74 +++++++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index d22ae4e2f3c..a97a59e44d5 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -205,14 +205,20 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I // the branch merges from a remote specified by URL r, err := ghrepo.FromURL(branchConfig.RemoteURL) if err != nil { - return 0, prHeadRef, err + // 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 r, err := rem.FindByName(branchConfig.RemoteName) if err != nil { - return 0, prHeadRef, err + // 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() } diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index eb40912b416..43cce5f4c93 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -2,7 +2,6 @@ package status import ( "bytes" - "fmt" "io" "net/http" "net/url" @@ -16,6 +15,7 @@ import ( fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" @@ -96,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) @@ -121,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) @@ -146,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) @@ -170,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) @@ -198,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) @@ -232,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) @@ -249,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) @@ -266,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) @@ -283,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) @@ -300,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) @@ -353,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) @@ -376,6 +431,15 @@ Requesting a code review from you } } +func TestPRStatus_error_ReadBranchConfig(t *testing.T) { + rs, cleanup := run.Stub() + defer cleanup(t) + rs.Register(`git config --get-regexp \^branch\\.`, 1, "") + + _, err := runCommand(initFakeHTTP(), "blueberries", true, "") + assert.Error(t, err) +} + func Test_prSelectorForCurrentBranch(t *testing.T) { tests := []struct { name string @@ -537,7 +601,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { wantError: nil, }, { - name: "Remote URL error", + name: "Remote URL errors", branchConfig: git.BranchConfig{ RemoteURL: &url.URL{ Scheme: "ssh", @@ -549,10 +613,10 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { prHeadRef: "monalisa/main", wantPrNumber: 0, wantSelector: "monalisa/main", - wantError: fmt.Errorf("invalid path: /\\invalid?Path/"), + wantError: nil, }, { - name: "Remote Name error", + name: "Remote Name errors", branchConfig: git.BranchConfig{ RemoteName: "nonexistentRemote", }, @@ -569,7 +633,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { }, wantPrNumber: 0, wantSelector: "monalisa/main", - wantError: fmt.Errorf("no matching remote found"), + wantError: nil, }, } From 1c32c986ece49dcc82b2335f9bac2a3607e842a2 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 10 Jan 2025 13:55:16 -0500 Subject: [PATCH 0171/2338] Capture greater detail on updaterEnabled This commit expands the in-line docs around updaterEnabled package variable used to affect release checking. Along with clarifying specific details discovered when talking with @williammartin, I'm also removing a useless local variable. --- internal/ghcmd/cmd.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/ghcmd/cmd.go b/internal/ghcmd/cmd.go index 5586b51c3e8..bd901b78e7b 100644 --- a/internal/ghcmd/cmd.go +++ b/internal/ghcmd/cmd.go @@ -31,6 +31,8 @@ import ( // updaterEnabled is a statically linked build property set in gh formula within homebrew/homebrew-core // used to control whether users are notified of newer GitHub CLI releases. +// This needs to be set to 'cli/cli' as it affects where update.CheckForUpdate() checks for releases. +// It is unclear whether this means that only homebrew builds will check for updates or not. // Development builds leave this empty as impossible to determine if newer or not. // For more information, . var updaterEnabled = "" @@ -227,9 +229,8 @@ func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion stri if err != nil { return nil, err } - repo := updaterEnabled stateFilePath := filepath.Join(config.StateDir(), "state.yml") - return update.CheckForUpdate(ctx, httpClient, stateFilePath, repo, currentVersion) + return update.CheckForUpdate(ctx, httpClient, stateFilePath, updaterEnabled, currentVersion) } func isRecentRelease(publishedAt time.Time) bool { From 20062233c8402d969da313d854903db7afa03379 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 10 Jan 2025 14:16:46 -0500 Subject: [PATCH 0172/2338] Separate logic for checking updates During discussion in cli/cli#9934, we can to the conclusion that the logic around checking for core GitHub CLI updates would diverge from GitHub CLI extension updates over time. To that end, this commit splits that logic into a separate function with a new environment variable. --- internal/update/update.go | 18 +++++++++-- internal/update/update_test.go | 57 ++++++++++++++++++++++++++++++++++ pkg/cmd/root/extension.go | 2 +- pkg/cmd/root/help_topic.go | 8 +++-- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/internal/update/update.go b/internal/update/update.go index 1afabaf21d0..a4a15ea17cc 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -33,9 +33,10 @@ type StateEntry struct { LatestRelease ReleaseInfo `yaml:"latest_release"` } -// ShouldCheckForUpdate decides whether we check for updates for the GitHub CLI and/or GitHub CLI extensions based on user preferences and current execution context. -func ShouldCheckForUpdate() bool { - if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { +// ShouldCheckForExtensionUpdate decides whether we check for updates for GitHub CLI extensions based on user preferences and current execution context. +// During cli/cli#9934, this logic was split out from ShouldCheckForUpdate() because we envisioned it going in a different direction. +func ShouldCheckForExtensionUpdate() bool { + if os.Getenv("GH_NO_EXTENSION_UPDATE_NOTIFIER") != "" { return false } if os.Getenv("CODESPACES") != "" { @@ -74,6 +75,17 @@ func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte return nil, nil } +// ShouldCheckForUpdate decides whether we check for updates for the GitHub CLI based on user preferences and current execution context. +func ShouldCheckForUpdate() bool { + if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { + return false + } + if os.Getenv("CODESPACES") != "" { + return false + } + return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr) +} + // CheckForUpdate checks whether an update exists for the GitHub CLI based on recency of last check within past 24 hours. func CheckForUpdate(ctx context.Context, client *http.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) { stateEntry, _ := getStateEntry(stateFilePath) diff --git a/internal/update/update_test.go b/internal/update/update_test.go index ebddee810b1..089e36f6885 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -425,6 +425,63 @@ func TestShouldCheckForUpdate(t *testing.T) { } } +func TestShouldCheckForExtensionUpdate(t *testing.T) { + tests := []struct { + name string + env map[string]string + expected bool + }{ + { + name: "should not check when user has explicitly disable notifications", + env: map[string]string{ + "GH_NO_EXTENSION_UPDATE_NOTIFIER": "1", + }, + expected: false, + }, + { + name: "should not check when user is in codespace", + env: map[string]string{ + "CODESPACES": "1", + }, + expected: false, + }, + { + name: "should not check when in GitHub Actions / Travis / Circle / Cirrus / GitLab / AppVeyor / CodeShip / dsari", + env: map[string]string{ + "CI": "1", + }, + expected: false, + }, + { + name: "should not check when in Jenkins / TeamCity", + env: map[string]string{ + "BUILD_NUMBER": "1", + }, + expected: false, + }, + { + name: "should not check when in TaskCluster / dsari", + env: map[string]string{ + "RUN_ID": "1", + }, + expected: false, + }, + // TODO: Figure out how to refactor IsTerminal() to be testable + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Clearenv() + for k, v := range tt.env { + os.Setenv(k, v) + } + + actual := ShouldCheckForExtensionUpdate() + require.Equal(t, tt.expected, actual) + }) + } +} + func tempFilePath() string { file, err := os.CreateTemp("", "") if err != nil { diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index b9cb3dfff81..7f2325e18c5 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -77,7 +77,7 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex } func checkForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension) (*update.ReleaseInfo, error) { - if !update.ShouldCheckForUpdate() { + if !update.ShouldCheckForExtensionUpdate() { return nil, nil } diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 7cca4cb7a6f..4359e421274 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -86,8 +86,12 @@ var HelpTopics = []helpTopic{ available in the viewport. When the value is a percentage, it will be applied against the number of columns available in the current viewport. - %[1]sGH_NO_UPDATE_NOTIFIER%[1]s: set to any value to disable update notifications. By default, gh - checks for new releases once every 24 hours and displays an upgrade notice on standard + %[1]sGH_NO_UPDATE_NOTIFIER%[1]s: set to any value to disable GitHub CLI update notifications. + By default, gh checks for new releases once every 24 hours and displays an upgrade notice on standard + error if a newer version was found. + + %[1]sGH_NO_EXTENSION_UPDATE_NOTIFIER%[1]s: set to any value to disable GitHub CLI extension update notifications. + By default, gh checks for new releases once every 24 hours and displays an upgrade notice on standard error if a newer version was found. %[1]sGH_CONFIG_DIR%[1]s: the directory where gh will store configuration files. If not specified, From 97eb8a194e465a0aeda3eba0af72210998dac93c Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 10 Jan 2025 14:29:55 -0500 Subject: [PATCH 0173/2338] Simplifying cleanExtensionUpdateDir logic This function was doing some unnecessary heavy lifting detecting if the directory being deleted actually existed when `os.RemoveAll()` would handle directories that exist or not. --- pkg/cmd/extension/manager.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 5881269cf07..e754306d828 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -851,14 +851,7 @@ func codesignBinary(binPath string) error { // cleanExtensionUpdateDir deletes extension state directory to avoid problems reinstalling extensions. func (m *Manager) cleanExtensionUpdateDir(name string) error { - updatePath := m.UpdateDir(name) - if _, err := os.Stat(updatePath); err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("failed to check previous extension update state: %w", err) - } - if err := os.RemoveAll(updatePath); err != nil { + if err := os.RemoveAll(m.UpdateDir(name)); err != nil { return fmt.Errorf("failed to remove previous extension update state: %w", err) } return nil From 0a1c9be656997623b08b424e8ebb528085dfe733 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 10 Jan 2025 14:57:10 -0500 Subject: [PATCH 0174/2338] Expand docs on cleaning extension update dir This summarizes a conversation within cli/cli#9934 PR regarding how the new extension update directory metadata would affect gh if not cleaned up. --- pkg/cmd/extension/manager.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index e754306d828..9a5fa5c1053 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -849,7 +849,20 @@ func codesignBinary(binPath string) error { return cmd.Run() } -// cleanExtensionUpdateDir deletes extension state directory to avoid problems reinstalling extensions. +// cleanExtensionUpdateDir deletes the extension-specific directory containing metadata used in checking for updates. +// Because extension names are not unique across GitHub organizations and users, we feel its important to clean up this metadata +// before installing or removing an extension with the same name to avoid confusing the extension manager based on past extensions. +// +// As of cli/cli#9934, the only effect on gh from not cleaning up metadata before installing or removing an extension are: +// +// 1. The last `checked_for_update_at` timestamp is sufficiently in the past and will check for an update on first use, +// which would happen if no extension update metadata existed. +// +// 2. The last `checked_for_update_at` timestamp is sufficiently in the future and will not check for an update, +// this is not a major concern as users could manually modify this. +// +// This could change over time as other functionality is added to extensions, which we cannot predict within cli/cli#9934, +// such as extension manifest and lock files within cli/cli#6118. func (m *Manager) cleanExtensionUpdateDir(name string) error { if err := os.RemoveAll(m.UpdateDir(name)); err != nil { return fmt.Errorf("failed to remove previous extension update state: %w", err) From 5de282c9702ca98bac0e4282bdb3ca6b12b40e71 Mon Sep 17 00:00:00 2001 From: danochoa Date: Sat, 11 Jan 2025 01:35:12 -0600 Subject: [PATCH 0175/2338] drop description from prompts, fix tests --- pkg/cmd/gist/delete/delete.go | 25 +++---- pkg/cmd/gist/delete/delete_test.go | 107 ++++++++++++++--------------- pkg/cmd/gist/shared/shared.go | 4 -- 3 files changed, 66 insertions(+), 70 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index af070b10ef5..ad1e0a48bbe 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -109,7 +109,7 @@ func deleteRun(opts *DeleteOptions) error { } if !opts.Confirmed { - confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete %q gist?", gist.FilenameDescription()), false) + confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete %q gist?", gist.Filename()), false) if err != nil { return err } @@ -119,18 +119,26 @@ func deleteRun(opts *DeleteOptions) error { } apiClient := api.NewClientFromHTTP(client) - if err := deleteGist(apiClient, host, gist, opts); err != nil { + if err := deleteGist(apiClient, host, gist.ID); err != nil { if errors.Is(err, shared.NotFoundErr) { - return fmt.Errorf("unable to delete gist %q: either the gist is not found or it is not owned by you", gist.FilenameDescription()) + 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 } -func deleteGist(apiClient *api.Client, hostname string, gist *shared.Gist, opts *DeleteOptions) error { - path := "gists/" + gist.ID +func deleteGist(apiClient *api.Client, hostname string, gistID string) error { + path := "gists/" + gistID err := apiClient.REST(hostname, "DELETE", path, nil, nil) if err != nil { var httpErr api.HTTPError @@ -139,12 +147,5 @@ func deleteGist(apiClient *api.Client, hostname string, gist *shared.Gist, opts } return err } - if opts.IO.IsStdoutTTY() { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, - "%s Gist %q deleted\n", - cs.SuccessIcon(), - gist.FilenameDescription()) - } return nil } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 729f81527fb..359e73a590e 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -117,14 +117,14 @@ func TestNewCmdDelete(t *testing.T) { func Test_deleteRun(t *testing.T) { tests := []struct { - name string - opts *DeleteOptions - cancel bool - httpStubs func(*httpmock.Registry) - mockGistList bool - wantErr bool - wantStdout string - wantStderr string + name string + opts *DeleteOptions + cancel bool + httpStubs func(*httpmock.Registry) + mockPromptGists bool + wantErr bool + wantStdout string + wantStderr string }{ { name: "successfully delete", @@ -133,12 +133,12 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) + 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, "{}")) }, wantErr: false, - wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", + wantStdout: "✓ Gist \"cool.txt\" deleted\n", wantStderr: "", }, { @@ -150,10 +150,10 @@ func Test_deleteRun(t *testing.T) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, - mockGistList: true, - wantErr: false, - wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", - wantStderr: "", + mockPromptGists: true, + wantErr: false, + wantStdout: "✓ Gist \"cool.txt\" deleted\n", + wantStderr: "", }, { name: "successfully delete with --yes", @@ -163,12 +163,12 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) + 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, "{}")) }, wantErr: false, - wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", + wantStdout: "✓ Gist \"cool.txt\" deleted\n", wantStderr: "", }, { @@ -181,10 +181,10 @@ func Test_deleteRun(t *testing.T) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, - mockGistList: true, - wantErr: false, - wantStdout: "✓ Gist \"cool.txt my cool text\" deleted\n", - wantStderr: "", + mockPromptGists: true, + wantErr: false, + wantStdout: "✓ Gist \"cool.txt\" deleted\n", + wantStderr: "", }, { name: "cancel delete with id", @@ -193,7 +193,7 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) }, cancel: true, wantErr: true, @@ -207,7 +207,7 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) + httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) }, cancel: true, wantErr: true, @@ -219,10 +219,12 @@ func Test_deleteRun(t *testing.T) { opts: &DeleteOptions{ Selector: "", }, - cancel: true, - wantErr: true, - wantStdout: "", - wantStderr: "", + httpStubs: func(reg *httpmock.Registry) {}, + mockPromptGists: true, + cancel: true, + wantErr: true, + wantStdout: "", + wantStderr: "", }, { name: "not found", @@ -231,52 +233,49 @@ func Test_deleteRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.JSONResponse(shared.Gist{ID: "1234", Description: "my cool text", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}})) + 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: "unable to delete gist \"cool.txt my cool text\": either the gist is not found or it is not owned by you", + wantStderr: "unable to delete gist \"cool.txt\": either the gist is not found or it is not owned by you", }, } for _, tt := range tests { pm := prompter.NewMockPrompter(t) if !tt.opts.Confirmed { - pm.RegisterConfirm("Delete \"cool.txt my cool text\" gist?", func(_ string, _ bool) (bool, error) { + pm.RegisterConfirm("Delete \"cool.txt\" gist?", func(_ string, _ bool) (bool, error) { return !tt.cancel, nil }) } reg := &httpmock.Registry{} - if !tt.cancel { - tt.httpStubs(reg) - 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": [ - { - "name": "1234", - "files": [{ "name": "cool.txt" }], - "description": "my cool text", - "updatedAt": "%s", - "isPublic": true - } - ] } } } }`, - sixHoursAgo.Format(time.RFC3339), - )), - ) - - pm.RegisterSelect("Select a gist", []string{"cool.txt my cool text about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { - return 0, nil - }) - } + tt.httpStubs(reg) + if tt.mockPromptGists { + 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) { diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index 085eec0abbb..53b577e4cb0 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -52,10 +52,6 @@ func (g Gist) TruncDescription() string { return text.Truncate(100, text.RemoveExcessiveWhitespace(g.Description)) } -func (g Gist) FilenameDescription() string { - return g.Filename() + " " + g.TruncDescription() -} - var NotFoundErr = errors.New("not found") func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) { From 13211af256aa73d36189a52d865af009e4c7c336 Mon Sep 17 00:00:00 2001 From: zhuhaicity Date: Sun, 12 Jan 2025 15:07:04 +0800 Subject: [PATCH 0176/2338] chore: fix some function names in comment Signed-off-by: zhuhaicity --- api/http_client.go | 2 +- pkg/cmdutil/errors.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/http_client.go b/api/http_client.go index e9381fa85b7..146b96df6b9 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -86,7 +86,7 @@ func AddCacheTTLHeader(rt http.RoundTripper, ttl time.Duration) http.RoundTrippe }} } -// AddAuthToken adds an authentication token header for the host specified by the request. +// AddAuthTokenHeader adds an authentication token header for the host specified by the request. func AddAuthTokenHeader(rt http.RoundTripper, cfg tokenGetter) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { // If the header is already set in the request, don't overwrite it. diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index 26a059d251b..edb97abcdb2 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -13,7 +13,7 @@ func FlagErrorf(format string, args ...interface{}) error { return FlagErrorWrap(fmt.Errorf(format, args...)) } -// FlagError returns a new FlagError that wraps the specified error. +// FlagErrorWrap returns a new FlagError that wraps the specified error. func FlagErrorWrap(err error) error { return &FlagError{err} } // A *FlagError indicates an error processing command-line flags or other arguments. From c5d6ae6cf698553e47b1fcd92385d7975296f35f Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Sun, 12 Jan 2025 14:53:20 -0300 Subject: [PATCH 0177/2338] Update pkg/cmd/gist/edit/edit.go Update pkg/cmd/gist/edit/edit.go after code review Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/gist/edit/edit.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index ced995f6329..5f2815682b7 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -111,6 +111,7 @@ func editRun(opts *EditOptions) error { if !opts.IO.CanPrompt() { return cmdutil.FlagErrorf("gist ID or URL required when not running interactively") } + gistID, err = shared.PromptGists(opts.Prompter, client, host, cs) if err != nil { return err From 24e9fed7e0720a47870a495bd54e45cca2b4a5a5 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Sun, 12 Jan 2025 14:57:24 -0300 Subject: [PATCH 0178/2338] removed unnecessary space --- pkg/cmd/gist/edit/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 5f2815682b7..b201cdc143e 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -111,7 +111,7 @@ func editRun(opts *EditOptions) error { if !opts.IO.CanPrompt() { return cmdutil.FlagErrorf("gist ID or URL required when not running interactively") } - + gistID, err = shared.PromptGists(opts.Prompter, client, host, cs) if err != nil { return err From 7885167c70eab766f9b887e2aa36bf166de10a09 Mon Sep 17 00:00:00 2001 From: danochoa Date: Sun, 12 Jan 2025 17:01:45 -0600 Subject: [PATCH 0179/2338] gist delete - check no gists --- pkg/cmd/gist/delete/delete.go | 4 ++ pkg/cmd/gist/delete/delete_test.go | 80 ++++++++++++++++-------------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index ad1e0a48bbe..8b8a47540a7 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -107,6 +107,10 @@ func deleteRun(opts *DeleteOptions) error { 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) diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 359e73a590e..97a7e11d3b6 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -122,6 +122,7 @@ func Test_deleteRun(t *testing.T) { cancel bool httpStubs func(*httpmock.Registry) mockPromptGists bool + noGists bool wantErr bool wantStdout string wantStderr string @@ -137,9 +138,7 @@ func Test_deleteRun(t *testing.T) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, - wantErr: false, wantStdout: "✓ Gist \"cool.txt\" deleted\n", - wantStderr: "", }, { name: "successfully delete with prompt", @@ -151,9 +150,7 @@ func Test_deleteRun(t *testing.T) { httpmock.StatusStringResponse(200, "{}")) }, mockPromptGists: true, - wantErr: false, wantStdout: "✓ Gist \"cool.txt\" deleted\n", - wantStderr: "", }, { name: "successfully delete with --yes", @@ -167,9 +164,7 @@ func Test_deleteRun(t *testing.T) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, - wantErr: false, wantStdout: "✓ Gist \"cool.txt\" deleted\n", - wantStderr: "", }, { name: "successfully delete with prompt and --yes", @@ -182,9 +177,7 @@ func Test_deleteRun(t *testing.T) { httpmock.StatusStringResponse(200, "{}")) }, mockPromptGists: true, - wantErr: false, wantStdout: "✓ Gist \"cool.txt\" deleted\n", - wantStderr: "", }, { name: "cancel delete with id", @@ -195,10 +188,8 @@ func Test_deleteRun(t *testing.T) { 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, - wantStdout: "", - wantStderr: "", + cancel: true, + wantErr: true, }, { name: "cancel delete with url", @@ -209,10 +200,8 @@ func Test_deleteRun(t *testing.T) { 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, - wantStdout: "", - wantStderr: "", + cancel: true, + wantErr: true, }, { name: "cancel delete with prompt", @@ -223,8 +212,6 @@ func Test_deleteRun(t *testing.T) { mockPromptGists: true, cancel: true, wantErr: true, - wantStdout: "", - wantStderr: "", }, { name: "not found", @@ -238,9 +225,18 @@ func Test_deleteRun(t *testing.T) { httpmock.StatusStringResponse(404, "{}")) }, wantErr: true, - wantStdout: "", wantStderr: "unable to delete gist \"cool.txt\": either the gist is not found or it is not owned by you", }, + { + name: "no gists", + opts: &DeleteOptions{ + Selector: "", + }, + httpStubs: func(reg *httpmock.Registry) {}, + mockPromptGists: true, + noGists: true, + wantStdout: "No gists found.\n", + }, } for _, tt := range tests { @@ -254,26 +250,34 @@ func Test_deleteRun(t *testing.T) { reg := &httpmock.Registry{} tt.httpStubs(reg) if tt.mockPromptGists { - 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), - )), - ) + 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 + }) + } - pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, _ []string) (int, error) { - return 0, nil - }) } tt.opts.Prompter = pm From 08f7706c4287b0bae04b5d8b2efb05ab01da23c0 Mon Sep 17 00:00:00 2001 From: danochoa Date: Sun, 12 Jan 2025 19:18:24 -0600 Subject: [PATCH 0180/2338] fix tests --- pkg/cmd/gist/delete/delete_test.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 97a7e11d3b6..fa311b07eb0 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -214,7 +214,7 @@ func Test_deleteRun(t *testing.T) { wantErr: true, }, { - name: "not found", + name: "not owned by you", opts: &DeleteOptions{ Selector: "1234", }, @@ -227,6 +227,19 @@ func Test_deleteRun(t *testing.T) { wantErr: true, 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, "{}")) + }, + noGists: true, + wantErr: true, + wantStderr: "not found", + }, { name: "no gists", opts: &DeleteOptions{ From 1d9b4e44640280ece40e6c97d136e5bfd6386a24 Mon Sep 17 00:00:00 2001 From: danochoa Date: Sun, 12 Jan 2025 19:36:44 -0600 Subject: [PATCH 0181/2338] fix tests --- pkg/cmd/gist/delete/delete_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index fa311b07eb0..74c511237a1 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -236,7 +236,6 @@ func Test_deleteRun(t *testing.T) { reg.Register(httpmock.REST("GET", "gists/1234"), httpmock.StatusStringResponse(404, "{}")) }, - noGists: true, wantErr: true, wantStderr: "not found", }, From 3ada5af872cdd4916649d2d33f0090951344d591 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 13 Jan 2025 08:41:10 -0500 Subject: [PATCH 0182/2338] Enhance help docs on ext upgrade notices - update `gh help environment` to include that upgrade notices only happen when gh or extensions are executed - update `gh ext --help` to include reference to upgrade notices and points to `gh help environment` for info on disabling --- pkg/cmd/extension/command.go | 3 +++ pkg/cmd/root/help_topic.go | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 410e96f5336..dad4021b643 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -46,6 +46,9 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { An extension cannot override any of the core gh commands. If an extension name conflicts with a core gh command, you can use %[1]sgh extension exec %[1]s. + When an extension is executed, gh will check for new versions once every 24 hours and display + an upgrade notice. See %[1]sgh help environment%[1]s for information on disabling extension notices. + For the list of available extensions, see . `, "`"), Aliases: []string{"extensions", "ext"}, diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 4359e421274..9ae2c5c8679 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -87,12 +87,12 @@ var HelpTopics = []helpTopic{ the number of columns available in the current viewport. %[1]sGH_NO_UPDATE_NOTIFIER%[1]s: set to any value to disable GitHub CLI update notifications. - By default, gh checks for new releases once every 24 hours and displays an upgrade notice on standard - error if a newer version was found. + When any command is executed, gh checks for new versions once every 24 hours. + If a newer version was found, an upgrade notice is displayed on standard error. %[1]sGH_NO_EXTENSION_UPDATE_NOTIFIER%[1]s: set to any value to disable GitHub CLI extension update notifications. - By default, gh checks for new releases once every 24 hours and displays an upgrade notice on standard - error if a newer version was found. + When an extension is executed, gh checks for new versions for the executed extension once every 24 hours. + If a newer version was found, an upgrade notice is displayed on standard error. %[1]sGH_CONFIG_DIR%[1]s: the directory where gh will store configuration files. If not specified, the default value will be one of the following paths (in order of precedence): From b41b77165bc8d0d6cdf7a031c62d2e09c3160aa7 Mon Sep 17 00:00:00 2001 From: Rajhans Jadhao Date: Mon, 13 Jan 2025 19:21:58 +0530 Subject: [PATCH 0183/2338] show error message for rerun workflow older than a month ago --- pkg/cmd/run/rerun/rerun.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 1c7d6b53f46..20e94b68ff7 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -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) From 977e2326a25d48c7fb4250b2927760ca28f18a79 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Mon, 13 Jan 2025 11:06:13 -0300 Subject: [PATCH 0184/2338] #10042: Applied code review suggestions to view_test.go --- pkg/cmd/gist/edit/edit_test.go | 7 ++++ pkg/cmd/gist/view/view_test.go | 58 +++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index 11a32bc92fb..0bf7cb51a17 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -519,6 +519,11 @@ func Test_editRun(t *testing.T) { }, }, }, + { + name: "no arguments notty", + nontty: true, + wantErr: "gist ID or URL required when not running interactively", + }, } for _, tt := range tests { @@ -552,6 +557,8 @@ func Test_editRun(t *testing.T) { stdin.WriteString(tt.stdin) ios.SetStdoutTTY(!tt.nontty) ios.SetStdinTTY(!tt.nontty) + ios.SetStderrTTY(!tt.nontty) + tt.opts.IO = ios tt.opts.Selector = "1234" diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 37dda42d118..820401f1e38 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -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) { @@ -116,19 +117,22 @@ func Test_viewRun(t *testing.T) { opts *ViewOptions wantOut string gist *shared.Gist - wantErr bool + isTTY bool + wantErr string mockGistList bool }{ { - 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, @@ -144,7 +148,8 @@ 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, @@ -158,10 +163,16 @@ func Test_viewRun(t *testing.T) { }, }, }, - wantErr: true, + 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", @@ -182,7 +193,8 @@ 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", @@ -204,7 +216,8 @@ 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, @@ -224,7 +237,8 @@ func Test_viewRun(t *testing.T) { 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, @@ -244,7 +258,8 @@ 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, @@ -265,7 +280,8 @@ func Test_viewRun(t *testing.T) { 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, @@ -287,7 +303,8 @@ 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, @@ -305,7 +322,8 @@ 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, @@ -377,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) From e916ae5b437486a12805c5e71e9f029e6d846634 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 13 Jan 2025 09:08:19 -0500 Subject: [PATCH 0185/2338] Rename domain pkg to shared --- pkg/cmd/repo/autolink/create/create.go | 6 +++--- pkg/cmd/repo/autolink/create/create_test.go | 6 +++--- pkg/cmd/repo/autolink/create/http.go | 6 +++--- pkg/cmd/repo/autolink/create/http_test.go | 6 +++--- pkg/cmd/repo/autolink/list/http.go | 6 +++--- pkg/cmd/repo/autolink/list/http_test.go | 8 ++++---- pkg/cmd/repo/autolink/list/list.go | 4 ++-- pkg/cmd/repo/autolink/list/list_test.go | 16 ++++++++-------- .../repo/autolink/{domain => shared}/autolink.go | 2 +- 9 files changed, 30 insertions(+), 30 deletions(-) rename pkg/cmd/repo/autolink/{domain => shared}/autolink.go (96%) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index 7c75f135239..6c321db5b16 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -6,7 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "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/spf13/cobra" @@ -25,7 +25,7 @@ type createOptions struct { } type AutolinkCreateClient interface { - Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) + Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) } func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command { @@ -114,7 +114,7 @@ func createRun(opts *createOptions) error { return nil } -func successMsg(autolink *domain.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { +func successMsg(autolink *shared.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { autolinkType := "Numeric" if autolink.IsAlphanumeric { autolinkType = "Alphanumeric" diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go index 3b31a9c7cb3..43fe3e4708b 100644 --- a/pkg/cmd/repo/autolink/create/create_test.go +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -9,7 +9,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "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" @@ -97,12 +97,12 @@ type stubAutoLinkCreator struct { err error } -func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { +func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) { if g.err != nil { return nil, g.err } - return &domain.Autolink{ + return &shared.Autolink{ ID: 1, KeyPrefix: request.KeyPrefix, URLTemplate: request.URLTemplate, diff --git a/pkg/cmd/repo/autolink/create/http.go b/pkg/cmd/repo/autolink/create/http.go index e4923c3f02c..d7ee940b907 100644 --- a/pkg/cmd/repo/autolink/create/http.go +++ b/pkg/cmd/repo/autolink/create/http.go @@ -10,7 +10,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" ) type AutolinkCreator struct { @@ -23,7 +23,7 @@ type AutolinkCreateRequest struct { URLTemplate string `json:"url_template"` } -func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { +func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path @@ -55,7 +55,7 @@ func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRe return nil, err } - var autolink domain.Autolink + var autolink shared.Autolink err = json.NewDecoder(resp.Body).Decode(&autolink) if err != nil { diff --git a/pkg/cmd/repo/autolink/create/http_test.go b/pkg/cmd/repo/autolink/create/http_test.go index 7f29f4611d1..9ef5e0da5ae 100644 --- a/pkg/cmd/repo/autolink/create/http_test.go +++ b/pkg/cmd/repo/autolink/create/http_test.go @@ -7,7 +7,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,7 +22,7 @@ func TestAutoLinkCreator_Create(t *testing.T) { stubStatus int stubRespJSON string - expectedAutolink *domain.Autolink + expectedAutolink *shared.Autolink expectErr bool expectedErrMsg string }{ @@ -40,7 +40,7 @@ func TestAutoLinkCreator_Create(t *testing.T) { "key_prefix": "TICKET-", "url_template": "https://example.com/TICKET?query=" }`, - expectedAutolink: &domain.Autolink{ + expectedAutolink: &shared.Autolink{ ID: 1, KeyPrefix: "TICKET-", URLTemplate: "https://example.com/TICKET?query=", diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index a5e707f4eeb..cdb8e621c61 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -8,14 +8,14 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" ) type AutolinkLister struct { HTTPClient *http.Client } -func (a *AutolinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { +func (a *AutolinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path req, err := http.NewRequest(http.MethodGet, url, nil) @@ -34,7 +34,7 @@ func (a *AutolinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } - var autolinks []domain.Autolink + var autolinks []shared.Autolink err = json.NewDecoder(resp.Body).Decode(&autolinks) if err != nil { return nil, err diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index 46f83484fc6..0fa918f6116 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,19 +16,19 @@ func TestAutoLinkLister_List(t *testing.T) { tests := []struct { name string repo ghrepo.Interface - resp []domain.Autolink + resp []shared.Autolink status int }{ { name: "no autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []domain.Autolink{}, + resp: []shared.Autolink{}, status: 200, }, { name: "two autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []domain.Autolink{ + resp: []shared.Autolink{ { ID: 1, IsAlphanumeric: true, diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 8d4a484e9c0..a68c03f3c94 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -9,7 +9,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/internal/text" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "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/spf13/cobra" @@ -33,7 +33,7 @@ type listOptions struct { } type AutolinkListClient interface { - List(repo ghrepo.Interface) ([]domain.Autolink, error) + List(repo ghrepo.Interface) ([]shared.Autolink, error) } func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 0ba350ab05c..1e4d73ab801 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -8,7 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "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/cli/cli/v2/pkg/jsonfieldstest" @@ -97,11 +97,11 @@ func TestNewCmdList(t *testing.T) { } type stubAutoLinkLister struct { - autolinks []domain.Autolink + autolinks []shared.Autolink err error } -func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { +func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) { return g.autolinks, g.err } @@ -126,7 +126,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -162,7 +162,7 @@ func TestListRun(t *testing.T) { }, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -185,7 +185,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: false, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -211,7 +211,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{}, + autolinks: []shared.Autolink{}, }, expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"), wantStderr: "", @@ -221,7 +221,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{}, + autolinks: []shared.Autolink{}, err: testAutolinkClientListError{}, }, expectedErr: testAutolinkClientListError{}, diff --git a/pkg/cmd/repo/autolink/domain/autolink.go b/pkg/cmd/repo/autolink/shared/autolink.go similarity index 96% rename from pkg/cmd/repo/autolink/domain/autolink.go rename to pkg/cmd/repo/autolink/shared/autolink.go index 9c3f10c05e1..d79537c3184 100644 --- a/pkg/cmd/repo/autolink/domain/autolink.go +++ b/pkg/cmd/repo/autolink/shared/autolink.go @@ -1,4 +1,4 @@ -package domain +package shared import "github.com/cli/cli/v2/pkg/cmdutil" From c0f6eb0598bad1463f8dca4b574c69342d653aa0 Mon Sep 17 00:00:00 2001 From: Mateus Marquezini Date: Mon, 13 Jan 2025 11:39:48 -0300 Subject: [PATCH 0186/2338] #10042: Attempt to add a new test scenario for handling error messages when TTY is unavailable --- pkg/cmd/gist/edit/edit_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index 0bf7cb51a17..be610b6b475 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -520,8 +520,11 @@ func Test_editRun(t *testing.T) { }, }, { - name: "no arguments notty", - nontty: true, + name: "no arguments notty", + nontty: true, + opts: &EditOptions{ + Selector: "", + }, wantErr: "gist ID or URL required when not running interactively", }, } From 7300f0d56888dd74605f8921aba8e796612ffcd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:23:32 +0000 Subject: [PATCH 0187/2338] Bump github.com/gabriel-vasile/mimetype from 1.4.7 to 1.4.8 Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.4.7 to 1.4.8. - [Release notes](https://github.com/gabriel-vasile/mimetype/releases) - [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.4.7...v1.4.8) --- updated-dependencies: - dependency-name: github.com/gabriel-vasile/mimetype dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5661ac09136..00d18bbd25f 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( 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/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 diff --git a/go.sum b/go.sum index d17d1251d10..16bcb6ed7b4 100644 --- a/go.sum +++ b/go.sum @@ -149,8 +149,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= From c7d04c980bd115e23f8c874a1c2b8d70bd15f872 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 08:34:15 -0700 Subject: [PATCH 0188/2338] update testing Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 59 +++++++++++-------- pkg/cmd/attestation/api/client_test.go | 40 ++++++++++++- .../attestation/api/mock_httpClient_test.go | 32 +++++++--- 3 files changed, 98 insertions(+), 33 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 37579b7bccf..2d491e10837 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -176,7 +176,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ return fmt.Errorf("attestation has no bundle or bundle URL") } - // for now, we fallback to the bundle field if the bundle URL is empty + // for now, we fall back to the bundle field if the bundle URL is empty if a.BundleURL == "" { c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") fetched[i] = &Attestation{ @@ -207,35 +207,46 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ func (c *LiveClient) GetBundle(url string) (*bundle.Bundle, error) { c.logger.VerbosePrintf("Fetching attestation bundle with bundle URL\n\n") - resp, err := c.httpClient.Get(url) - if err != nil { - return nil, err - } + var sgBundle *bundle.Bundle + bo := backoff.NewConstantBackOff(getAttestationRetryInterval) + err := backoff.Retry(func() error { + resp, err := c.httpClient.Get(url) + if err != nil { + return backoff.Permanent(err) + } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } + if resp.StatusCode >= 500 && resp.StatusCode <= 599 { + return fmt.Errorf("attestation bundle with URL %s returned status code %d", url, resp.StatusCode) + } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read blob storage response body: %w", err) - } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read blob storage response body: %w", err) + } - var out []byte - decompressed, err := snappy.Decode(out, body) - if err != nil { - return nil, fmt.Errorf("failed to decompress with snappy: %w", err) - } + var out []byte + decompressed, err := snappy.Decode(out, body) + if err != nil { + fmt.Errorf("failed to decompress with snappy: %w", err) + } - var pbBundle v1.Bundle - if err = protojson.Unmarshal(decompressed, &pbBundle); err != nil { - return nil, fmt.Errorf("failed to unmarshal to bundle: %w", err) - } + var pbBundle v1.Bundle + if err = protojson.Unmarshal(decompressed, &pbBundle); err != nil { + return fmt.Errorf("failed to unmarshal to bundle: %w", err) + } - c.logger.VerbosePrintf("Successfully fetched bundle\n\n") + c.logger.VerbosePrintf("Successfully fetched bundle\n\n") + + sgBundle, err = bundle.NewBundle(&pbBundle) + if err != nil { + return fmt.Errorf("failed to create new bundle: %w", err) + } + + return nil + }, backoff.WithMaxRetries(bo, 3)) - return bundle.NewBundle(&pbBundle) + return sgBundle, err } func shouldRetry(err error) bool { diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 65f8d59ca3c..96edbf06175 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -212,7 +212,10 @@ func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { } func TestFetchBundleFromAttestations_Fail(t *testing.T) { - httpClient := &failAfterOneCallHttpClient{} + httpClient := &failAfterNCallsHttpClient{ + FailOnCallN: 2, + FailOnAllSubsequentCalls: true, + } c := &LiveClient{ httpClient: httpClient, @@ -225,7 +228,7 @@ func TestFetchBundleFromAttestations_Fail(t *testing.T) { fetched, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) require.Nil(t, fetched) - httpClient.AssertNumberOfCalls(t, "OnGetFailAfterOneCall", 2) + httpClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2) } func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { @@ -244,6 +247,22 @@ func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) } +func TestFetchBundleFromAttestations_FetchByURL5XX_Resp(t *testing.T) { + mockHTTPClient := &failHttpClient{} + + c := &LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), + } + + a := makeTestAttestation() + attestations := []*Attestation{&a} + bundle, err := c.fetchBundleFromAttestations(attestations) + require.Error(t, err) + require.Nil(t, bundle) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 3) +} + func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { mockHTTPClient := &mockHttpClient{} @@ -260,6 +279,23 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { mockHTTPClient.AssertNotCalled(t, "OnGetSuccess") } +func TestGetBundle(t *testing.T) { + mockHTTPClient := &failAfterNCallsHttpClient{ + FailOnCallN: 2, + FailOnAllSubsequentCalls: false, + } + + c := &LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), + } + + b, err := c.GetBundle("whatever") + require.NoError(t, err) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 3) +} + func TestGetTrustDomain(t *testing.T) { fetcher := mockMetaGenerator{ TrustDomain: "foo", diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 4dc34fbf8e7..074979c2a5d 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -40,21 +40,39 @@ func (m *failHttpClient) Get(url string) (*http.Response, error) { }, fmt.Errorf("failed to fetch with %s", url) } -type failAfterOneCallHttpClient struct { +type failHttpClient5XX struct { mock.Mock } -func (m *failAfterOneCallHttpClient) Get(url string) (*http.Response, error) { - m.On("OnGetFailAfterOneCall").Return() +func (m *failHttpClient5XX) Get(url string) (*http.Response, error) { + m.On("OnGetFail").Return() + m.MethodCalled("OnGetFail") + + return &http.Response{ + StatusCode: 500, + }, nil +} + +type failAfterNCallsHttpClient struct { + mock.Mock + FailOnCallN int + FailOnAllSubsequentCalls bool + NumCalls int +} + +func (m *failAfterNCallsHttpClient) Get(url string) (*http.Response, error) { + m.On("OnGetFailAfterNCalls").Return() + + m.NumCalls++ - if len(m.Calls) >= 1 { - m.MethodCalled("OnGetFailAfterOneCall") + if m.NumCalls == m.FailOnCallN || (m.NumCalls > m.FailOnCallN && m.FailOnAllSubsequentCalls) { + m.MethodCalled("OnGetFailAfterNCalls") return &http.Response{ StatusCode: 500, - }, fmt.Errorf("failed to fetch with %s", url) + }, nil } - m.MethodCalled("OnGetFailAfterOneCall") + m.MethodCalled("OnGetFailAfterNCalls") var compressed []byte compressed = snappy.Encode(compressed, data.SigstoreBundleRaw) return &http.Response{ From 4d99ae920c6b056f9077f5e538dd7600738b1795 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 10:34:06 -0700 Subject: [PATCH 0189/2338] fix tests Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 60 ++++++++++++++++++-------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 96edbf06175..9728b265c2a 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -211,27 +211,22 @@ func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { require.Nil(t, fetched, 2) } -func TestFetchBundleFromAttestations_Fail(t *testing.T) { - httpClient := &failAfterNCallsHttpClient{ - FailOnCallN: 2, - FailOnAllSubsequentCalls: true, - } +func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { + mockHTTPClient := &failHttpClient{} c := &LiveClient{ - httpClient: httpClient, + httpClient: mockHTTPClient, logger: io.NewTestHandler(), } - att1 := makeTestAttestation() - att2 := makeTestAttestation() - attestations := []*Attestation{&att1, &att2} - fetched, err := c.fetchBundleFromAttestations(attestations) + a := makeTestAttestation() + attestations := []*Attestation{&a} + bundle, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) - require.Nil(t, fetched) - httpClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2) + require.Nil(t, bundle) } -func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { +func TestFetchBundleFromAttestations_FailWithGetErr(t *testing.T) { mockHTTPClient := &failHttpClient{} c := &LiveClient{ @@ -248,7 +243,10 @@ func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { } func TestFetchBundleFromAttestations_FetchByURL5XX_Resp(t *testing.T) { - mockHTTPClient := &failHttpClient{} + mockHTTPClient := &failAfterNCallsHttpClient{ + FailOnCallN: 1, + FailOnAllSubsequentCalls: true, + } c := &LiveClient{ httpClient: mockHTTPClient, @@ -260,10 +258,9 @@ func TestFetchBundleFromAttestations_FetchByURL5XX_Resp(t *testing.T) { bundle, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) require.Nil(t, bundle) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 3) } -func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { +func TestFetchBundleFromAttestations_FallbackToBundleField(t *testing.T) { mockHTTPClient := &mockHttpClient{} c := &LiveClient{ @@ -280,8 +277,22 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) { } func TestGetBundle(t *testing.T) { + mockHTTPClient := &mockHttpClient{} + + c := &LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), + } + + b, err := c.GetBundle("whatever") + require.NoError(t, err) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) +} + +func TestGetBundle_Retry(t *testing.T) { mockHTTPClient := &failAfterNCallsHttpClient{ - FailOnCallN: 2, + FailOnCallN: 1, FailOnAllSubsequentCalls: false, } @@ -293,7 +304,20 @@ func TestGetBundle(t *testing.T) { b, err := c.GetBundle("whatever") require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 3) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2) +} + +func TestGetBundle_Fail(t *testing.T) { + mockHTTPClient := &failHttpClient{} + c := &LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), + } + + b, err := c.GetBundle("whatever") + require.Error(t, err) + require.Nil(t, b) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) } func TestGetTrustDomain(t *testing.T) { From 40e7353b52a1da047c8ff984a79823239d04a670 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 11:02:33 -0700 Subject: [PATCH 0190/2338] deduplicate get attestation code Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 2d491e10837..526916bcd69 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -67,18 +67,9 @@ func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { // GetByRepoAndDigest fetches the attestation by repo and digest func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { + c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) url := c.BuildRepoAndDigestURL(repo, digest) - attestations, err := c.getAttestations(url, repo, digest, limit) - if err != nil { - return nil, err - } - - bundles, err := c.fetchBundleFromAttestations(attestations) - if err != nil { - return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) - } - - return bundles, nil + return c.getByURL(url, limit) } func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { @@ -88,16 +79,17 @@ func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { // GetByOwnerAndDigest fetches attestation by owner and digest func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { + c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) url := c.BuildOwnerAndDigestURL(owner, digest) - attestations, err := c.getAttestations(url, owner, digest, limit) + return c.getByURL(url, limit) +} + +func (c *LiveClient) getByURL(url string, limit int) ([]*Attestation, error) { + attestations, err := c.getAttestations(url, limit) if err != nil { return nil, err } - if len(attestations) == 0 { - return nil, newErrNoAttestations(owner, digest) - } - bundles, err := c.fetchBundleFromAttestations(attestations) if err != nil { return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err) @@ -112,9 +104,7 @@ func (c *LiveClient) GetTrustDomain() (string, error) { return c.getTrustDomain(MetaPath) } -func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) { - c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) - +func (c *LiveClient) getAttestations(url string, limit int) ([]*Attestation, error) { perPage := limit if perPage <= 0 || perPage > maxLimitForFlag { return nil, fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag) @@ -157,7 +147,7 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At } if len(attestations) == 0 { - return nil, newErrNoAttestations(name, digest) + return nil, ErrNoAttestations{} } if len(attestations) > limit { From 5462582401fcc68b3e4a7b13004fec2227715704 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 11:05:17 -0700 Subject: [PATCH 0191/2338] drop unneeded methods Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 14 ++--------- pkg/cmd/attestation/api/client_test.go | 34 +++++++++++++------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 526916bcd69..c3670f42dc4 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -60,27 +60,17 @@ func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClien } } -func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { - repo = strings.Trim(repo, "/") - return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest) -} - // GetByRepoAndDigest fetches the attestation by repo and digest func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) - url := c.BuildRepoAndDigestURL(repo, digest) + url := fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest) return c.getByURL(url, limit) } -func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { - owner = strings.Trim(owner, "/") - return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest) -} - // GetByOwnerAndDigest fetches attestation by owner and digest func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) - url := c.BuildOwnerAndDigestURL(owner, digest) + url := fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest) return c.getByURL(url, limit) } diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 9728b265c2a..b93d53b7046 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -42,23 +42,23 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { } } -func TestGetURL(t *testing.T) { - c := LiveClient{} - - testData := []struct { - repo string - digest string - expected string - }{ - {repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, - {repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, - } - - for _, data := range testData { - s := c.BuildRepoAndDigestURL(data.repo, data.digest) - require.Equal(t, data.expected, s) - } -} +//func TestGetURL(t *testing.T) { +// c := LiveClient{} +// +// testData := []struct { +// repo string +// digest string +// expected string +// }{ +// {repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, +// {repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, +// } +// +// for _, data := range testData { +// s := c.BuildRepoAndDigestURL(data.repo, data.digest) +// require.Equal(t, data.expected, s) +// } +//} func TestGetByDigest(t *testing.T) { c := NewClientWithMockGHClient(false) From 960f533234dcf327e1e9456c417f0b81ad09647c Mon Sep 17 00:00:00 2001 From: AMS21 Date: Mon, 13 Jan 2025 20:01:28 +0100 Subject: [PATCH 0192/2338] add install instructions for Manjaro Linux --- docs/install_linux.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/install_linux.md b/docs/install_linux.md index 33339d6df9e..43ba876a174 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -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 From fc0d0210c0cfcc37d5f45f8f3a92893690c5b587 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 12:21:27 -0700 Subject: [PATCH 0193/2338] remove old tests Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index b93d53b7046..77cd22ee6c1 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -42,24 +42,6 @@ func NewClientWithMockGHClient(hasNextPage bool) Client { } } -//func TestGetURL(t *testing.T) { -// c := LiveClient{} -// -// testData := []struct { -// repo string -// digest string -// expected string -// }{ -// {repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, -// {repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, -// } -// -// for _, data := range testData { -// s := c.BuildRepoAndDigestURL(data.repo, data.digest) -// require.Equal(t, data.expected, s) -// } -//} - func TestGetByDigest(t *testing.T) { c := NewClientWithMockGHClient(false) attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) From b7f6af03b59f9a697c879a0fda06e44fb134c07b Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 12:42:10 -0700 Subject: [PATCH 0194/2338] update no attestations found err Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/attestation.go | 16 ++-------------- pkg/cmd/attestation/api/client.go | 2 +- pkg/cmd/attestation/api/client_test.go | 4 ++-- pkg/cmd/attestation/api/mock_httpClient_test.go | 13 ------------- pkg/cmd/attestation/download/download.go | 2 +- pkg/cmd/attestation/download/download_test.go | 2 +- pkg/cmd/attestation/verify/verify.go | 2 +- 7 files changed, 8 insertions(+), 33 deletions(-) diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go index 16b062a9689..fd6b484a7cd 100644 --- a/pkg/cmd/attestation/api/attestation.go +++ b/pkg/cmd/attestation/api/attestation.go @@ -1,8 +1,7 @@ package api import ( - "fmt" - + "errors" "github.com/sigstore/sigstore-go/pkg/bundle" ) @@ -11,18 +10,7 @@ const ( GetAttestationByOwnerAndSubjectDigestPath = "orgs/%s/attestations/%s" ) -type ErrNoAttestations struct { - name string - digest string -} - -func (e ErrNoAttestations) Error() string { - return fmt.Sprintf("no attestations found for digest %s in %s", e.name, e.digest) -} - -func newErrNoAttestations(name, digest string) ErrNoAttestations { - return ErrNoAttestations{name, digest} -} +var ErrNoAttestationsFound = errors.New("no attestations found") type Attestation struct { Bundle *bundle.Bundle `json:"bundle"` diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index c3670f42dc4..faa1609fca1 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -137,7 +137,7 @@ func (c *LiveClient) getAttestations(url string, limit int) ([]*Attestation, err } if len(attestations) == 0 { - return nil, ErrNoAttestations{} + return nil, ErrNoAttestationsFound } if len(attestations) > limit { diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 77cd22ee6c1..5a7c299c759 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -132,12 +132,12 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) { attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) require.Error(t, err) - require.IsType(t, ErrNoAttestations{}, err) + require.IsType(t, ErrNoAttestationsFound, err) require.Nil(t, attestations) attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) require.Error(t, err) - require.IsType(t, ErrNoAttestations{}, err) + require.IsType(t, ErrNoAttestationsFound, err) require.Nil(t, attestations) } diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index 074979c2a5d..e11266a6073 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -40,19 +40,6 @@ func (m *failHttpClient) Get(url string) (*http.Response, error) { }, fmt.Errorf("failed to fetch with %s", url) } -type failHttpClient5XX struct { - mock.Mock -} - -func (m *failHttpClient5XX) Get(url string) (*http.Response, error) { - m.On("OnGetFail").Return() - m.MethodCalled("OnGetFail") - - return &http.Response{ - StatusCode: 500, - }, nil -} - type failAfterNCallsHttpClient struct { mock.Mock FailOnCallN int diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 7547e9e68ab..cdbdc007874 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -135,7 +135,7 @@ func runDownload(opts *Options) error { } attestations, err := verification.GetRemoteAttestations(opts.APIClient, params) if err != nil { - if errors.Is(err, api.ErrNoAttestations{}) { + if errors.Is(err, api.ErrNoAttestationsFound) { fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath) return nil } diff --git a/pkg/cmd/attestation/download/download_test.go b/pkg/cmd/attestation/download/download_test.go index 6c2986065c3..ddcd08c9280 100644 --- a/pkg/cmd/attestation/download/download_test.go +++ b/pkg/cmd/attestation/download/download_test.go @@ -276,7 +276,7 @@ func TestRunDownload(t *testing.T) { opts := baseOpts opts.APIClient = api.MockClient{ OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) { - return nil, api.ErrNoAttestations{} + return nil, api.ErrNoAttestationsFound }, } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 016ec1fa8d1..a5e78a48e58 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -220,7 +220,7 @@ func runVerify(opts *Options) error { attestations, logMsg, err := getAttestations(opts, *artifact) if err != nil { - if ok := errors.Is(err, api.ErrNoAttestations{}); ok { + if ok := errors.Is(err, api.ErrNoAttestationsFound); ok { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ No attestations found for subject %s\n"), artifact.DigestWithAlg()) return err } From 611eb86e68d2bd2dc44cd801af11f2ea514ab8c1 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 12:47:25 -0700 Subject: [PATCH 0195/2338] method update Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 4 ++-- pkg/cmd/attestation/api/client_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index faa1609fca1..f6991261c85 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -166,7 +166,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ } // otherwise fetch the bundle with the provided URL - b, err := c.GetBundle(a.BundleURL) + b, err := c.getBundle(a.BundleURL) if err != nil { return fmt.Errorf("failed to fetch bundle with URL: %w", err) } @@ -184,7 +184,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ return fetched, nil } -func (c *LiveClient) GetBundle(url string) (*bundle.Bundle, error) { +func (c *LiveClient) getBundle(url string) (*bundle.Bundle, error) { c.logger.VerbosePrintf("Fetching attestation bundle with bundle URL\n\n") var sgBundle *bundle.Bundle diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 5a7c299c759..f89dcddf2ff 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -266,7 +266,7 @@ func TestGetBundle(t *testing.T) { logger: io.NewTestHandler(), } - b, err := c.GetBundle("whatever") + b, err := c.getBundle("whatever") require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) @@ -283,7 +283,7 @@ func TestGetBundle_Retry(t *testing.T) { logger: io.NewTestHandler(), } - b, err := c.GetBundle("whatever") + b, err := c.getBundle("whatever") require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2) @@ -296,7 +296,7 @@ func TestGetBundle_Fail(t *testing.T) { logger: io.NewTestHandler(), } - b, err := c.GetBundle("whatever") + b, err := c.getBundle("whatever") require.Error(t, err) require.Nil(t, b) mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) From 1d807c22915282d5d576c6406828ef60ec5eb0db Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 12:50:58 -0700 Subject: [PATCH 0196/2338] add missing return statement Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index f6991261c85..359bdad54d9 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -208,7 +208,7 @@ func (c *LiveClient) getBundle(url string) (*bundle.Bundle, error) { var out []byte decompressed, err := snappy.Decode(out, body) if err != nil { - fmt.Errorf("failed to decompress with snappy: %w", err) + return fmt.Errorf("failed to decompress with snappy: %w", err) } var pbBundle v1.Bundle From 2ddfe865f4b41618c3005ff81d02d6c890ad3372 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 13 Jan 2025 13:02:05 -0700 Subject: [PATCH 0197/2338] Update pkg/cmd/attestation/verify/verify.go Co-authored-by: Phill MV --- pkg/cmd/attestation/verify/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index b0d82be3c8a..7b6250c0c47 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -262,7 +262,7 @@ func runVerify(opts *Options) error { return nil } - opts.Logger.Printf("%s was attested by %s with the following attributes:\n\n", artifact.DigestWithAlg(), text.Pluralize(len(verified), "attestation")) + opts.Logger.Printf("The following %d %s matched the policy criteria:\n\n", len(verified), text.Pluralize(len(verified), "attestation")) // Otherwise print the results to the terminal in a table tableContent, err := buildTableVerifyContent(opts.Tenant, verified) From bc8c46b0b1c1fd411e286990b950aad254cfb78f Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 13 Jan 2025 20:38:03 -0500 Subject: [PATCH 0198/2338] Make extension update check non-blocking Fixes #10235 This commit updates the Cobra command logic around extension upgrade checks to be non-blocking. Previously, we were waiting for 1 second after the extension completed to allow the update checking logic complete, however users want the GitHub CLI to run as far as possible. --- pkg/cmd/root/extension.go | 31 ++++++++++++--------- pkg/cmd/root/extension_test.go | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go index 7f2325e18c5..7e2d7aca75f 100644 --- a/pkg/cmd/root/extension.go +++ b/pkg/cmd/root/extension.go @@ -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", diff --git a/pkg/cmd/root/extension_test.go b/pkg/cmd/root/extension_test.go index 32b250de585..7dd66df3101 100644 --- a/pkg/cmd/root/extension_test.go +++ b/pkg/cmd/root/extension_test.go @@ -3,6 +3,7 @@ package root_test import ( "io" "testing" + "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/update" @@ -121,6 +122,8 @@ 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 1 second to allow update checking logic to complete. + time.Sleep(1 * time.Second) return true, nil }, } @@ -169,3 +172,51 @@ func TestNewCmdExtension_Updates(t *testing.T) { } } } + +func TestNewCmdExtension_UpdateCheckIsNonblocking(t *testing.T) { + ios, _, _, stderr := 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" + }, + } + + checkFunc := func(em extensions.ExtensionManager, ext extensions.Extension) (*update.ReleaseInfo, error) { + // This function runs in the background as a goroutine while the extension is dispatched. + // Testing whether the extension cobra command is non-blocking for extension update check, + // this function will cause goroutine to sleep for a sufficiently long time before panicking. + // The idea is that the test will conclude because the dispatch function completes immediately + // and exits before the check update function completes. + time.Sleep(30 * time.Second) + panic("It seems like the extension cobra command might be blocking on update checking when it should not block on long update checks") + } + + cmd := root.NewCmdExtension(ios, em, ext, checkFunc) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + require.Emptyf(t, stderr.String(), "long running, non-blocking extension update check should not have displayed anything on stderr") +} From 3feeb9a826e31cd6afbb884a4deeade37c802364 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 14 Jan 2025 11:41:20 -0800 Subject: [PATCH 0199/2338] Add tests for deleteGist --- pkg/cmd/gist/delete/delete_test.go | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 74c511237a1..24ca2bb33c2 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -7,6 +7,7 @@ import ( "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" @@ -14,6 +15,7 @@ import ( "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" ) @@ -322,3 +324,71 @@ func Test_deleteRun(t *testing.T) { }) } } + +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) + } + + }) + } +} From 018438088e111df9cdc1a36ec11141de85a0bbe6 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 14 Jan 2025 16:02:41 -0800 Subject: [PATCH 0200/2338] Add missing git stubs to tests --- pkg/cmd/pr/status/status_test.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 19a5a451925..97524acbaa4 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -100,6 +100,8 @@ func TestPRStatus(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -130,6 +132,8 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -160,6 +164,8 @@ func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{}) if err != nil { @@ -189,6 +195,8 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -222,6 +230,8 @@ func TestPRStatus_currentBranch_defaultBranch(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -261,6 +271,8 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -283,6 +295,8 @@ func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -305,6 +319,8 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -327,6 +343,8 @@ func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -349,6 +367,8 @@ func TestPRStatus_blankSlate(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -407,6 +427,8 @@ func TestPRStatus_detachedHead(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") + rs.Register(`git config remote.pushDefault`, 0, "") + rs.Register(`git rev-parse --verify --quiet --abbrev-ref @{push}`, 0, "") output, err := runCommand(http, "", true, "") if err != nil { @@ -434,7 +456,8 @@ Requesting a code review from you func TestPRStatus_error_ReadBranchConfig(t *testing.T) { rs, cleanup := run.Stub() defer cleanup(t) - rs.Register(`git config --get-regexp \^branch\\.`, 1, "") + // We only need the one stub because this fails early + rs.Register(`git config --get-regexp \^branch\\.`, 2, "") _, err := runCommand(initFakeHTTP(), "blueberries", true, "") assert.Error(t, err) } From 88988374b51ae71952c708a4d01a814b40c564ee Mon Sep 17 00:00:00 2001 From: Wing Date: Wed, 4 Dec 2024 15:24:41 +0100 Subject: [PATCH 0201/2338] Add remote check to secret commands Co-authored-by: William Martin --- pkg/cmd/secret/delete/delete.go | 12 ++++++++++++ pkg/cmd/secret/list/list.go | 12 ++++++++++++ pkg/cmd/secret/set/set.go | 13 +++++++++++++ pkg/cmdutil/errors.go | 16 ++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 564e8633fa8..75d061e7f69 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -6,6 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/secret/shared" @@ -19,12 +20,15 @@ type DeleteOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) + Remotes func() (ghContext.Remotes, error) SecretName string OrgName string EnvName string UserSecrets bool Application string + + HasRepoOverride bool } func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command { @@ -32,6 +36,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, + Remotes: f.Remotes, } cmd := &cobra.Command{ @@ -53,6 +58,8 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co return err } + opts.HasRepoOverride = cmd.Flags().Changed("repo") + opts.SecretName = args[0] if runF != nil { @@ -103,6 +110,11 @@ func removeRun(opts *DeleteOptions) error { if err != nil { return err } + + err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) + if err != nil { + return err + } } cfg, err := opts.Config() diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 9e1fd305d04..b87c31998cb 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -9,6 +9,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" @@ -23,6 +24,7 @@ type ListOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) + Remotes func() (ghContext.Remotes, error) Now func() time.Time Exporter cmdutil.Exporter @@ -30,6 +32,8 @@ type ListOptions struct { EnvName string UserSecrets bool Application string + + HasRepoOverride bool } var secretFields = []string{ @@ -47,6 +51,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, + Remotes: f.Remotes, Now: time.Now, } @@ -70,6 +75,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman return err } + opts.HasRepoOverride = cmd.Flags().Changed("repo") + if runF != nil { return runF(opts) } @@ -101,6 +108,11 @@ func listRun(opts *ListOptions) error { if err != nil { return err } + + err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) + if err != nil { + return err + } } secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 755bdda1f8b..dade133af2a 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -11,6 +11,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/secret/shared" @@ -27,6 +28,7 @@ type SetOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) + Remotes func() (ghContext.Remotes, error) Prompter iprompter RandomOverride func() io.Reader @@ -41,6 +43,8 @@ type SetOptions struct { RepositoryNames []string EnvFile string Application string + + HasRepoOverride bool } type iprompter interface { @@ -52,6 +56,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, + Remotes: f.Remotes, Prompter: f.Prompter, } @@ -144,6 +149,8 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command } } + opts.HasRepoOverride = cmd.Flags().Changed("repo") + if runF != nil { return runF(opts) } @@ -187,6 +194,12 @@ func setRun(opts *SetOptions) error { if err != nil { return err } + + err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) + if err != nil { + return err + } + host = baseRepo.RepoHost() } else { cfg, err := opts.Config() diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index edb97abcdb2..389a40d25c1 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2/terminal" + ghContext "github.com/cli/cli/v2/context" ) // FlagErrorf returns a new FlagError that wraps an error produced by @@ -57,6 +58,21 @@ func MutuallyExclusive(message string, conditions ...bool) error { return nil } +func ValidateHasOnlyOneRemote(hasRepoOverride bool, remotes func() (ghContext.Remotes, error)) error { + if !hasRepoOverride { + remotes, err := remotes() + if err != nil { + return err + } + + if remotes.Len() > 1 { + return fmt.Errorf("multiple remotes detected %v. please specify which repo to use by providing the -R or --repo argument", remotes) + } + } + + return nil +} + type NoResultsError struct { message string } From 57c9ee0ad2257d4b68a90764cb2d973f4e04dcbc Mon Sep 17 00:00:00 2001 From: Wing Date: Wed, 4 Dec 2024 15:25:47 +0100 Subject: [PATCH 0202/2338] Add tests for secret commands Co-authored-by: William Martin --- pkg/cmd/secret/delete/delete_test.go | 118 +++++++++++++++++++ pkg/cmd/secret/list/list_test.go | 169 +++++++++++++++++++++++++++ pkg/cmd/secret/set/set_test.go | 139 ++++++++++++++++++++++ pkg/cmdutil/errors.go | 2 +- 4 files changed, 427 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/secret/delete/delete_test.go b/pkg/cmd/secret/delete/delete_test.go index bd5053f4d59..45091a3b83e 100644 --- a/pkg/cmd/secret/delete/delete_test.go +++ b/pkg/cmd/secret/delete/delete_test.go @@ -5,6 +5,8 @@ import ( "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" @@ -351,3 +353,119 @@ func Test_removeRun_user(t *testing.T) { reg.Verify(t) } + +func Test_removeRun_remote_validation(t *testing.T) { + tests := []struct { + name string + opts *DeleteOptions + wantPath string + wantErr bool + errMsg string + }{ + { + name: "single repo detected", + opts: &DeleteOptions{ + Application: "actions", + SecretName: "cool_secret", + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + }, nil + }}, + wantPath: "repos/owner/repo/actions/secrets/cool_secret", + }, + { + name: "multi repo detected", + opts: &DeleteOptions{ + Application: "actions", + SecretName: "cool_secret", + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + remote2 := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "upstream", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + remote2, + }, nil + }}, + wantErr: true, + errMsg: "multiple remotes detected [origin upstream]. please specify which repo to use by providing the -R or --repo argument", + }, + { + name: "multi repo detected - single repo given", + opts: &DeleteOptions{ + Application: "actions", + SecretName: "cool_secret", + HasRepoOverride: true, + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + remote2 := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "upstream", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + remote2, + }, nil + }}, + wantPath: "repos/owner/repo/actions/secrets/cool_secret", + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + + if tt.wantPath != "" { + reg.Register( + httpmock.REST("DELETE", tt.wantPath), + httpmock.StatusStringResponse(204, "No Content")) + } + + ios, _, _, _ := iostreams.Test() + + tt.opts.IO = ios + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + tt.opts.Config = func() (gh.Config, error) { + return config.NewBlankConfig(), nil + } + tt.opts.BaseRepo = func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + } + + err := removeRun(tt.opts) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + } else { + assert.NoError(t, err) + } + + reg.Verify(t) + } +} diff --git a/pkg/cmd/secret/list/list_test.go b/pkg/cmd/secret/list/list_test.go index 2e7b1906531..a66b913ea99 100644 --- a/pkg/cmd/secret/list/list_test.go +++ b/pkg/cmd/secret/list/list_test.go @@ -9,6 +9,8 @@ import ( "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" @@ -442,6 +444,173 @@ func Test_listRun(t *testing.T) { } } +func Test_listRunRemoteValidation(t *testing.T) { + tests := []struct { + name string + tty bool + json bool + opts *ListOptions + wantOut []string + wantErr bool + errMsg string + }{ + { + name: "single repo detected", + tty: false, + opts: &ListOptions{ + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + }, nil + }, + }, + wantOut: []string{ + "SECRET_ONE\t1988-10-11T00:00:00Z", + "SECRET_TWO\t2020-12-04T00:00:00Z", + "SECRET_THREE\t1975-11-30T00:00:00Z", + }, + }, + { + name: "multi repo detected", + tty: false, + opts: &ListOptions{ + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + remote2 := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "upstream", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + remote2, + }, nil + }, + }, + wantOut: []string{}, + wantErr: true, + errMsg: "multiple remotes detected [origin upstream]. please specify which repo to use by providing the -R or --repo argument", + }, + { + name: "multi repo detected - single repo given", + tty: false, + opts: &ListOptions{ + HasRepoOverride: true, + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + remote2 := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "upstream", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + remote2, + }, nil + }, + }, + wantOut: []string{ + "SECRET_ONE\t1988-10-11T00:00:00Z", + "SECRET_TWO\t2020-12-04T00:00:00Z", + "SECRET_THREE\t1975-11-30T00:00:00Z", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + reg.Verify(t) + + path := "repos/owner/repo/actions/secrets" + + t0, _ := time.Parse("2006-01-02", "1988-10-11") + t1, _ := time.Parse("2006-01-02", "2020-12-04") + t2, _ := time.Parse("2006-01-02", "1975-11-30") + payload := struct { + Secrets []Secret + }{ + Secrets: []Secret{ + { + Name: "SECRET_ONE", + UpdatedAt: t0, + }, + { + Name: "SECRET_TWO", + UpdatedAt: t1, + }, + { + Name: "SECRET_THREE", + UpdatedAt: t2, + }, + }, + } + + reg.Register(httpmock.REST("GET", path), httpmock.JSONResponse(payload)) + + ios, _, stdout, _ := iostreams.Test() + + ios.SetStdoutTTY(tt.tty) + + tt.opts.IO = ios + tt.opts.BaseRepo = func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + } + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + tt.opts.Config = func() (gh.Config, error) { + return config.NewBlankConfig(), nil + } + tt.opts.Now = func() time.Time { + t, _ := time.Parse(time.RFC822, "15 Mar 23 00:00 UTC") + return t + } + + if tt.json { + exporter := cmdutil.NewJSONExporter() + exporter.SetFields(secretFields) + tt.opts.Exporter = exporter + } + + err := listRun(tt.opts) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + + return + } + + assert.NoError(t, err) + + if len(tt.wantOut) > 1 { + expected := fmt.Sprintf("%s\n", strings.Join(tt.wantOut, "\n")) + assert.Equal(t, expected, stdout.String()) + } + }) + } +} + // Test_listRun_populatesNumSelectedReposIfRequired asserts that NumSelectedRepos // field is populated **only** when it's going to be presented in the output. Since // populating this field costs further API requests (one per secret), it's important diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 5c8d6f693d4..68c7624dd25 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -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" @@ -700,6 +702,143 @@ func Test_getSecretsFromOptions(t *testing.T) { } } +func Test_setRun_remote_validation(t *testing.T) { + tests := []struct { + name string + opts *SetOptions + wantApp string + wantErr bool + errMsg string + }{ + { + name: "single repo detected", + opts: &SetOptions{ + Application: "actions", + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + }, nil + }, + }, + wantApp: "actions", + }, + { + name: "multi repo detected", + opts: &SetOptions{ + Application: "actions", + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + remote2 := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "upstream", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + remote2, + }, nil + }, + }, + wantErr: true, + errMsg: "multiple remotes detected [origin upstream]. please specify which repo to use by providing the -R or --repo argument", + }, + { + name: "multi repo detected - single repo given", + opts: &SetOptions{ + Application: "actions", + HasRepoOverride: true, + Remotes: func() (ghContext.Remotes, error) { + remote := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("owner", "repo"), + } + remote2 := &ghContext.Remote{ + Remote: &git.Remote{ + Name: "upstream", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return ghContext.Remotes{ + remote, + remote2, + }, nil + }, + }, + wantApp: "actions", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + + if tt.wantApp != "" { + reg.Register(httpmock.REST("GET", fmt.Sprintf("repos/owner/repo/%s/secrets/public-key", tt.wantApp)), + httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="})) + + reg.Register(httpmock.REST("PUT", fmt.Sprintf("repos/owner/repo/%s/secrets/cool_secret", tt.wantApp)), + httpmock.StatusStringResponse(201, `{}`)) + } + + ios, _, _, _ := iostreams.Test() + + opts := &SetOptions{ + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + }, + IO: ios, + SecretName: "cool_secret", + Body: "a secret", + RandomOverride: fakeRandom, + Application: tt.opts.Application, + HasRepoOverride: tt.opts.HasRepoOverride, + Remotes: tt.opts.Remotes, + } + + err := setRun(opts) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + } else { + assert.NoError(t, err) + } + + reg.Verify(t) + + if tt.wantApp != "" && !tt.wantErr { + data, err := io.ReadAll(reg.Requests[1].Body) + assert.NoError(t, err) + + var payload SecretPayload + err = json.Unmarshal(data, &payload) + assert.NoError(t, err) + assert.Equal(t, payload.KeyID, "123") + assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=") + } + }) + } +} + func fakeRandom() io.Reader { return bytes.NewReader(bytes.Repeat([]byte{5}, 32)) } diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index 389a40d25c1..e1a37d0c816 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -59,7 +59,7 @@ func MutuallyExclusive(message string, conditions ...bool) error { } func ValidateHasOnlyOneRemote(hasRepoOverride bool, remotes func() (ghContext.Remotes, error)) error { - if !hasRepoOverride { + if !hasRepoOverride && remotes != nil { remotes, err := remotes() if err != nil { return err From 3de2fd94b3caea368c5bd9d0273c0eb47ffc7091 Mon Sep 17 00:00:00 2001 From: Wing Date: Wed, 4 Dec 2024 15:27:01 +0100 Subject: [PATCH 0203/2338] Prompt for secret commands Co-authored-by: William Martin --- pkg/cmd/secret/set/set.go | 29 +++++++++++++++++++++++------ pkg/cmdutil/flags.go | 8 ++++++++ pkg/cmdutil/repo_override.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index dade133af2a..9f37970080d 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/base64" "fmt" + "github.com/cli/cli/v2/internal/prompter" "io" "net/http" "os" @@ -29,7 +30,7 @@ type SetOptions struct { Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) Remotes func() (ghContext.Remotes, error) - Prompter iprompter + Prompter prompter.Prompter RandomOverride func() io.Reader @@ -45,10 +46,7 @@ type SetOptions struct { Application string HasRepoOverride bool -} - -type iprompter interface { - Password(string) (string, error) + Interactive bool } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -82,6 +80,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 @@ -111,6 +112,8 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command // support `-R, --repo` override opts.BaseRepo = f.BaseRepo + flagCount := cmdutil.CountSetFlags(cmd.Flags()) + if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil { return err } @@ -129,6 +132,10 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command } } else { opts.SecretName = args[0] + + if flagCount == 0 { + opts.Interactive = true + } } if cmd.Flags().Changed("visibility") { @@ -197,7 +204,17 @@ func setRun(opts *SetOptions) error { err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) if err != nil { - return err + if opts.Interactive { + selectedRepo, errSelectedRepo := cmdutil.PromptForRepo(baseRepo, opts.Remotes, opts.Prompter) + + if errSelectedRepo != nil { + return errSelectedRepo + } + + baseRepo = selectedRepo + } else { + return err + } } host = baseRepo.RepoHost() diff --git a/pkg/cmdutil/flags.go b/pkg/cmdutil/flags.go index c0064099cdd..fadbc5fbed7 100644 --- a/pkg/cmdutil/flags.go +++ b/pkg/cmdutil/flags.go @@ -180,3 +180,11 @@ func isIncluded(value string, opts []string) bool { } return false } + +func CountSetFlags(flags *pflag.FlagSet) int { + count := 0 + flags.Visit(func(f *pflag.Flag) { + count++ + }) + return count +} diff --git a/pkg/cmdutil/repo_override.go b/pkg/cmdutil/repo_override.go index 791dd919a21..41a4a47998a 100644 --- a/pkg/cmdutil/repo_override.go +++ b/pkg/cmdutil/repo_override.go @@ -1,11 +1,13 @@ package cmdutil import ( + ghContext "github.com/cli/cli/v2/context" "os" "sort" "strings" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" "github.com/spf13/cobra" ) @@ -68,3 +70,34 @@ func OverrideBaseRepoFunc(f *Factory, override string) func() (ghrepo.Interface, } return f.BaseRepo } + +func PromptForRepo(baseRepo ghrepo.Interface, remotes func() (ghContext.Remotes, error), survey prompter.Prompter) (ghrepo.Interface, error) { + var defaultRepo string + var remoteArray []string + + if remotes, _ := remotes(); remotes != nil { + if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { + // this is a remote explicitly chosen via `repo set-default` + defaultRepo = ghrepo.FullName(defaultRemote) + } else if len(remotes) > 0 { + // as a fallback, just pick the first remote + defaultRepo = ghrepo.FullName(remotes[0]) + } + + for _, remote := range remotes { + remoteArray = append(remoteArray, ghrepo.FullName(remote)) + } + } + + baseRepoInput, errInput := survey.Select("Select a base repo", defaultRepo, remoteArray) + if errInput != nil { + return baseRepo, errInput + } + + selectedRepo, errSelectedRepo := ghrepo.FromFullName(remoteArray[baseRepoInput]) + if errSelectedRepo != nil { + return baseRepo, errSelectedRepo + } + + return selectedRepo, nil +} From cad59036f5aabb62e70723fb8ac7da61b34ed719 Mon Sep 17 00:00:00 2001 From: Wing Date: Wed, 21 Aug 2024 18:16:06 +0200 Subject: [PATCH 0204/2338] Update docs for set-default NOTE: gh does not use the default repository for managing repository and environment secrets. --- pkg/cmd/repo/setdefault/setdefault.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/repo/setdefault/setdefault.go b/pkg/cmd/repo/setdefault/setdefault.go index eb8fcfb5a57..69be625d627 100644 --- a/pkg/cmd/repo/setdefault/setdefault.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -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 or variables.`) } type iprompter interface { From 0be5720c1cd1ffacebead919be7091e53d1e17b6 Mon Sep 17 00:00:00 2001 From: Wing Date: Wed, 21 Aug 2024 18:31:27 +0200 Subject: [PATCH 0205/2338] Update setdefault test --- pkg/cmd/repo/setdefault/setdefault.go | 2 +- pkg/cmd/repo/setdefault/setdefault_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/repo/setdefault/setdefault.go b/pkg/cmd/repo/setdefault/setdefault.go index 69be625d627..3e1554a6933 100644 --- a/pkg/cmd/repo/setdefault/setdefault.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -31,7 +31,7 @@ func explainer() string { - viewing and creating releases - working with GitHub Actions - ### NOTE: gh does not use the default repository for managing repository and environment secrets or variables.`) + ### NOTE: gh does not use the default repository for managing repository and environment secrets.`) } type iprompter interface { diff --git a/pkg/cmd/repo/setdefault/setdefault_test.go b/pkg/cmd/repo/setdefault/setdefault_test.go index 55a0193d593..dfd72d83e76 100644 --- a/pkg/cmd/repo/setdefault/setdefault_test.go +++ b/pkg/cmd/repo/setdefault/setdefault_test.go @@ -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", }, } From df8bb51c9cf92ac3ae72fb914799e004f54d6b10 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 9 Dec 2024 15:59:01 +0100 Subject: [PATCH 0206/2338] Always prompt on secret set when multiple remotes --- pkg/cmd/secret/set/set.go | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 9f37970080d..74bd7b33c5c 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/base64" "fmt" - "github.com/cli/cli/v2/internal/prompter" "io" "net/http" "os" "strings" + "github.com/cli/cli/v2/internal/prompter" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" ghContext "github.com/cli/cli/v2/context" @@ -46,7 +47,7 @@ type SetOptions struct { Application string HasRepoOverride bool - Interactive bool + CanPrompt bool } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -112,8 +113,6 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command // support `-R, --repo` override opts.BaseRepo = f.BaseRepo - flagCount := cmdutil.CountSetFlags(cmd.Flags()) - if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil { return err } @@ -132,10 +131,6 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command } } else { opts.SecretName = args[0] - - if flagCount == 0 { - opts.Interactive = true - } } if cmd.Flags().Changed("visibility") { @@ -157,6 +152,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command } opts.HasRepoOverride = cmd.Flags().Changed("repo") + opts.CanPrompt = opts.IO.CanPrompt() if runF != nil { return runF(opts) @@ -202,19 +198,17 @@ func setRun(opts *SetOptions) error { return err } - err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) - if err != nil { - if opts.Interactive { - selectedRepo, errSelectedRepo := cmdutil.PromptForRepo(baseRepo, opts.Remotes, opts.Prompter) - - if errSelectedRepo != nil { - return errSelectedRepo - } - - baseRepo = selectedRepo - } else { + if err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes); err != nil { + if !opts.CanPrompt { return err } + + selectedRepo, errSelectingRepo := cmdutil.PromptForRepo(baseRepo, opts.Remotes, opts.Prompter) + if errSelectingRepo != nil { + return errSelectingRepo + } + + baseRepo = selectedRepo } host = baseRepo.RepoHost() From 73244c010eddcb89534b090203059d1ee3dd4dd5 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 9 Dec 2024 16:04:41 +0100 Subject: [PATCH 0207/2338] Move secret repo validation into secrets subpackage --- pkg/cmd/secret/delete/delete.go | 2 +- pkg/cmd/secret/list/list.go | 2 +- pkg/cmd/secret/set/set.go | 4 +-- pkg/cmd/secret/shared/base_repo.go | 55 ++++++++++++++++++++++++++++++ pkg/cmdutil/errors.go | 16 --------- pkg/cmdutil/repo_override.go | 33 ------------------ 6 files changed, 59 insertions(+), 53 deletions(-) create mode 100644 pkg/cmd/secret/shared/base_repo.go diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 75d061e7f69..70aab4be593 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -111,7 +111,7 @@ func removeRun(opts *DeleteOptions) error { return err } - err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) + err = shared.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) if err != nil { return err } diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index b87c31998cb..5c13234d03e 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -109,7 +109,7 @@ func listRun(opts *ListOptions) error { return err } - err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) + err = shared.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) if err != nil { return err } diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 74bd7b33c5c..b4630e0677b 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -198,12 +198,12 @@ func setRun(opts *SetOptions) error { return err } - if err = cmdutil.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes); err != nil { + if err = shared.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes); err != nil { if !opts.CanPrompt { return err } - selectedRepo, errSelectingRepo := cmdutil.PromptForRepo(baseRepo, opts.Remotes, opts.Prompter) + selectedRepo, errSelectingRepo := shared.PromptForRepo(baseRepo, opts.Remotes, opts.Prompter) if errSelectingRepo != nil { return errSelectingRepo } diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go new file mode 100644 index 00000000000..db2dfc8f1b1 --- /dev/null +++ b/pkg/cmd/secret/shared/base_repo.go @@ -0,0 +1,55 @@ +package shared + +import ( + "fmt" + + ghContext "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" +) + +func ValidateHasOnlyOneRemote(hasRepoOverride bool, remotes func() (ghContext.Remotes, error)) error { + if !hasRepoOverride && remotes != nil { + remotes, err := remotes() + if err != nil { + return err + } + + if remotes.Len() > 1 { + return fmt.Errorf("multiple remotes detected %v. please specify which repo to use by providing the -R or --repo argument", remotes) + } + } + + return nil +} + +func PromptForRepo(baseRepo ghrepo.Interface, remotes func() (ghContext.Remotes, error), survey prompter.Prompter) (ghrepo.Interface, error) { + var defaultRepo string + var remoteArray []string + + if remotes, _ := remotes(); remotes != nil { + if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { + // this is a remote explicitly chosen via `repo set-default` + defaultRepo = ghrepo.FullName(defaultRemote) + } else if len(remotes) > 0 { + // as a fallback, just pick the first remote + defaultRepo = ghrepo.FullName(remotes[0]) + } + + for _, remote := range remotes { + remoteArray = append(remoteArray, ghrepo.FullName(remote)) + } + } + + baseRepoInput, errInput := survey.Select("Select a base repo", defaultRepo, remoteArray) + if errInput != nil { + return baseRepo, errInput + } + + selectedRepo, errSelectedRepo := ghrepo.FromFullName(remoteArray[baseRepoInput]) + if errSelectedRepo != nil { + return baseRepo, errSelectedRepo + } + + return selectedRepo, nil +} diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index e1a37d0c816..edb97abcdb2 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2/terminal" - ghContext "github.com/cli/cli/v2/context" ) // FlagErrorf returns a new FlagError that wraps an error produced by @@ -58,21 +57,6 @@ func MutuallyExclusive(message string, conditions ...bool) error { return nil } -func ValidateHasOnlyOneRemote(hasRepoOverride bool, remotes func() (ghContext.Remotes, error)) error { - if !hasRepoOverride && remotes != nil { - remotes, err := remotes() - if err != nil { - return err - } - - if remotes.Len() > 1 { - return fmt.Errorf("multiple remotes detected %v. please specify which repo to use by providing the -R or --repo argument", remotes) - } - } - - return nil -} - type NoResultsError struct { message string } diff --git a/pkg/cmdutil/repo_override.go b/pkg/cmdutil/repo_override.go index 41a4a47998a..791dd919a21 100644 --- a/pkg/cmdutil/repo_override.go +++ b/pkg/cmdutil/repo_override.go @@ -1,13 +1,11 @@ package cmdutil import ( - ghContext "github.com/cli/cli/v2/context" "os" "sort" "strings" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/internal/prompter" "github.com/spf13/cobra" ) @@ -70,34 +68,3 @@ func OverrideBaseRepoFunc(f *Factory, override string) func() (ghrepo.Interface, } return f.BaseRepo } - -func PromptForRepo(baseRepo ghrepo.Interface, remotes func() (ghContext.Remotes, error), survey prompter.Prompter) (ghrepo.Interface, error) { - var defaultRepo string - var remoteArray []string - - if remotes, _ := remotes(); remotes != nil { - if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { - // this is a remote explicitly chosen via `repo set-default` - defaultRepo = ghrepo.FullName(defaultRemote) - } else if len(remotes) > 0 { - // as a fallback, just pick the first remote - defaultRepo = ghrepo.FullName(remotes[0]) - } - - for _, remote := range remotes { - remoteArray = append(remoteArray, ghrepo.FullName(remote)) - } - } - - baseRepoInput, errInput := survey.Select("Select a base repo", defaultRepo, remoteArray) - if errInput != nil { - return baseRepo, errInput - } - - selectedRepo, errSelectedRepo := ghrepo.FromFullName(remoteArray[baseRepoInput]) - if errSelectedRepo != nil { - return baseRepo, errSelectedRepo - } - - return selectedRepo, nil -} From 870da79886e418e7c095247653440635edc0fd99 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 18 Dec 2024 13:36:53 +0100 Subject: [PATCH 0208/2338] Use smarter base repo funcs for secret commands --- pkg/cmd/secret/delete/delete.go | 25 ++- pkg/cmd/secret/delete/delete_test.go | 221 +++++++++---------- pkg/cmd/secret/list/list.go | 33 +-- pkg/cmd/secret/list/list_test.go | 271 +++++++++--------------- pkg/cmd/secret/set/set.go | 35 ++- pkg/cmd/secret/set/set_test.go | 240 +++++++++------------ pkg/cmd/secret/shared/base_repo.go | 73 +++++-- pkg/cmd/secret/shared/base_repo_test.go | 239 +++++++++++++++++++++ pkg/cmdutil/flags.go | 8 - 9 files changed, 646 insertions(+), 499 deletions(-) create mode 100644 pkg/cmd/secret/shared/base_repo_test.go diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 70aab4be593..64b02c9d964 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" - ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/secret/shared" @@ -20,15 +19,12 @@ type DeleteOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) - Remotes func() (ghContext.Remotes, error) SecretName string OrgName string EnvName string UserSecrets bool Application string - - HasRepoOverride bool } func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command { @@ -36,7 +32,6 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, - Remotes: f.Remotes, } cmd := &cobra.Command{ @@ -51,15 +46,24 @@ 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 error 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.PromptWhenMultipleRemotesBaseRepoFunc(opts.BaseRepo, f.Prompter) + } + } if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil { return err } - opts.HasRepoOverride = cmd.Flags().Changed("repo") - opts.SecretName = args[0] if runF != nil { @@ -110,11 +114,6 @@ func removeRun(opts *DeleteOptions) error { if err != nil { return err } - - err = shared.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) - if err != nil { - return err - } } cfg, err := opts.Config() diff --git a/pkg/cmd/secret/delete/delete_test.go b/pkg/cmd/secret/delete/delete_test.go index 45091a3b83e..91d688a8954 100644 --- a/pkg/cmd/secret/delete/delete_test.go +++ b/pkg/cmd/secret/delete/delete_test.go @@ -2,6 +2,7 @@ package delete import ( "bytes" + "io" "net/http" "testing" @@ -10,6 +11,8 @@ import ( "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" @@ -122,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.MultipleRemotesError{ + 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 base 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 @@ -353,119 +458,3 @@ func Test_removeRun_user(t *testing.T) { reg.Verify(t) } - -func Test_removeRun_remote_validation(t *testing.T) { - tests := []struct { - name string - opts *DeleteOptions - wantPath string - wantErr bool - errMsg string - }{ - { - name: "single repo detected", - opts: &DeleteOptions{ - Application: "actions", - SecretName: "cool_secret", - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - }, nil - }}, - wantPath: "repos/owner/repo/actions/secrets/cool_secret", - }, - { - name: "multi repo detected", - opts: &DeleteOptions{ - Application: "actions", - SecretName: "cool_secret", - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - remote2 := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "upstream", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - remote2, - }, nil - }}, - wantErr: true, - errMsg: "multiple remotes detected [origin upstream]. please specify which repo to use by providing the -R or --repo argument", - }, - { - name: "multi repo detected - single repo given", - opts: &DeleteOptions{ - Application: "actions", - SecretName: "cool_secret", - HasRepoOverride: true, - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - remote2 := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "upstream", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - remote2, - }, nil - }}, - wantPath: "repos/owner/repo/actions/secrets/cool_secret", - }, - } - - for _, tt := range tests { - reg := &httpmock.Registry{} - - if tt.wantPath != "" { - reg.Register( - httpmock.REST("DELETE", tt.wantPath), - httpmock.StatusStringResponse(204, "No Content")) - } - - ios, _, _, _ := iostreams.Test() - - tt.opts.IO = ios - tt.opts.HttpClient = func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - } - tt.opts.Config = func() (gh.Config, error) { - return config.NewBlankConfig(), nil - } - tt.opts.BaseRepo = func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("owner/repo") - } - - err := removeRun(tt.opts) - if tt.wantErr { - assert.EqualError(t, err, tt.errMsg) - } else { - assert.NoError(t, err) - } - - reg.Verify(t) - } -} diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 5c13234d03e..49ad47aff8f 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -9,9 +9,9 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" - ghContext "github.com/cli/cli/v2/context" "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" @@ -24,16 +24,15 @@ type ListOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) - Remotes func() (ghContext.Remotes, error) - Now func() time.Time - Exporter cmdutil.Exporter + Prompter prompter.Prompter + + Now func() time.Time + Exporter cmdutil.Exporter OrgName string EnvName string UserSecrets bool Application string - - HasRepoOverride bool } var secretFields = []string{ @@ -51,8 +50,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, - Remotes: f.Remotes, Now: time.Now, + Prompter: f.Prompter, } cmd := &cobra.Command{ @@ -68,15 +67,24 @@ 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 error 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.PromptWhenMultipleRemotesBaseRepoFunc(opts.BaseRepo, f.Prompter) + } + } if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil { return err } - opts.HasRepoOverride = cmd.Flags().Changed("repo") - if runF != nil { return runF(opts) } @@ -108,11 +116,6 @@ func listRun(opts *ListOptions) error { if err != nil { return err } - - err = shared.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes) - if err != nil { - return err - } } secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) diff --git a/pkg/cmd/secret/list/list_test.go b/pkg/cmd/secret/list/list_test.go index a66b913ea99..63a899b7746 100644 --- a/pkg/cmd/secret/list/list_test.go +++ b/pkg/cmd/secret/list/list_test.go @@ -3,6 +3,7 @@ package list import ( "bytes" "fmt" + "io" "net/http" "net/url" "strings" @@ -14,6 +15,7 @@ import ( "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" @@ -103,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.MultipleRemotesError{ + 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 base 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 @@ -444,173 +548,6 @@ func Test_listRun(t *testing.T) { } } -func Test_listRunRemoteValidation(t *testing.T) { - tests := []struct { - name string - tty bool - json bool - opts *ListOptions - wantOut []string - wantErr bool - errMsg string - }{ - { - name: "single repo detected", - tty: false, - opts: &ListOptions{ - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - }, nil - }, - }, - wantOut: []string{ - "SECRET_ONE\t1988-10-11T00:00:00Z", - "SECRET_TWO\t2020-12-04T00:00:00Z", - "SECRET_THREE\t1975-11-30T00:00:00Z", - }, - }, - { - name: "multi repo detected", - tty: false, - opts: &ListOptions{ - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - remote2 := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "upstream", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - remote2, - }, nil - }, - }, - wantOut: []string{}, - wantErr: true, - errMsg: "multiple remotes detected [origin upstream]. please specify which repo to use by providing the -R or --repo argument", - }, - { - name: "multi repo detected - single repo given", - tty: false, - opts: &ListOptions{ - HasRepoOverride: true, - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - remote2 := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "upstream", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - remote2, - }, nil - }, - }, - wantOut: []string{ - "SECRET_ONE\t1988-10-11T00:00:00Z", - "SECRET_TWO\t2020-12-04T00:00:00Z", - "SECRET_THREE\t1975-11-30T00:00:00Z", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reg := &httpmock.Registry{} - reg.Verify(t) - - path := "repos/owner/repo/actions/secrets" - - t0, _ := time.Parse("2006-01-02", "1988-10-11") - t1, _ := time.Parse("2006-01-02", "2020-12-04") - t2, _ := time.Parse("2006-01-02", "1975-11-30") - payload := struct { - Secrets []Secret - }{ - Secrets: []Secret{ - { - Name: "SECRET_ONE", - UpdatedAt: t0, - }, - { - Name: "SECRET_TWO", - UpdatedAt: t1, - }, - { - Name: "SECRET_THREE", - UpdatedAt: t2, - }, - }, - } - - reg.Register(httpmock.REST("GET", path), httpmock.JSONResponse(payload)) - - ios, _, stdout, _ := iostreams.Test() - - ios.SetStdoutTTY(tt.tty) - - tt.opts.IO = ios - tt.opts.BaseRepo = func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("owner/repo") - } - tt.opts.HttpClient = func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - } - tt.opts.Config = func() (gh.Config, error) { - return config.NewBlankConfig(), nil - } - tt.opts.Now = func() time.Time { - t, _ := time.Parse(time.RFC822, "15 Mar 23 00:00 UTC") - return t - } - - if tt.json { - exporter := cmdutil.NewJSONExporter() - exporter.SetFields(secretFields) - tt.opts.Exporter = exporter - } - - err := listRun(tt.opts) - if tt.wantErr { - assert.EqualError(t, err, tt.errMsg) - - return - } - - assert.NoError(t, err) - - if len(tt.wantOut) > 1 { - expected := fmt.Sprintf("%s\n", strings.Join(tt.wantOut, "\n")) - assert.Equal(t, expected, stdout.String()) - } - }) - } -} - // Test_listRun_populatesNumSelectedReposIfRequired asserts that NumSelectedRepos // field is populated **only** when it's going to be presented in the output. Since // populating this field costs further API requests (one per secret), it's important diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index b4630e0677b..ba9b023a6b0 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -13,7 +13,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" - ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/secret/shared" @@ -30,7 +29,6 @@ type SetOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) BaseRepo func() (ghrepo.Interface, error) - Remotes func() (ghContext.Remotes, error) Prompter prompter.Prompter RandomOverride func() io.Reader @@ -45,9 +43,6 @@ type SetOptions struct { RepositoryNames []string EnvFile string Application string - - HasRepoOverride bool - CanPrompt bool } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -55,7 +50,6 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, - Remotes: f.Remotes, Prompter: f.Prompter, } @@ -110,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 error 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.PromptWhenMultipleRemotesBaseRepoFunc(opts.BaseRepo, f.Prompter) + } + } if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil { return err @@ -151,9 +156,6 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command } } - opts.HasRepoOverride = cmd.Flags().Changed("repo") - opts.CanPrompt = opts.IO.CanPrompt() - if runF != nil { return runF(opts) } @@ -198,19 +200,6 @@ func setRun(opts *SetOptions) error { return err } - if err = shared.ValidateHasOnlyOneRemote(opts.HasRepoOverride, opts.Remotes); err != nil { - if !opts.CanPrompt { - return err - } - - selectedRepo, errSelectingRepo := shared.PromptForRepo(baseRepo, opts.Remotes, opts.Prompter) - if errSelectingRepo != nil { - return errSelectingRepo - } - - baseRepo = selectedRepo - } - host = baseRepo.RepoHost() } else { cfg, err := opts.Config() diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 68c7624dd25..63834521ffd 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -22,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) { @@ -223,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.MultipleRemotesError{ + 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 base 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 @@ -702,143 +805,6 @@ func Test_getSecretsFromOptions(t *testing.T) { } } -func Test_setRun_remote_validation(t *testing.T) { - tests := []struct { - name string - opts *SetOptions - wantApp string - wantErr bool - errMsg string - }{ - { - name: "single repo detected", - opts: &SetOptions{ - Application: "actions", - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - }, nil - }, - }, - wantApp: "actions", - }, - { - name: "multi repo detected", - opts: &SetOptions{ - Application: "actions", - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - remote2 := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "upstream", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - remote2, - }, nil - }, - }, - wantErr: true, - errMsg: "multiple remotes detected [origin upstream]. please specify which repo to use by providing the -R or --repo argument", - }, - { - name: "multi repo detected - single repo given", - opts: &SetOptions{ - Application: "actions", - HasRepoOverride: true, - Remotes: func() (ghContext.Remotes, error) { - remote := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "origin", - }, - Repo: ghrepo.New("owner", "repo"), - } - remote2 := &ghContext.Remote{ - Remote: &git.Remote{ - Name: "upstream", - }, - Repo: ghrepo.New("owner", "repo"), - } - - return ghContext.Remotes{ - remote, - remote2, - }, nil - }, - }, - wantApp: "actions", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reg := &httpmock.Registry{} - - if tt.wantApp != "" { - reg.Register(httpmock.REST("GET", fmt.Sprintf("repos/owner/repo/%s/secrets/public-key", tt.wantApp)), - httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="})) - - reg.Register(httpmock.REST("PUT", fmt.Sprintf("repos/owner/repo/%s/secrets/cool_secret", tt.wantApp)), - httpmock.StatusStringResponse(201, `{}`)) - } - - ios, _, _, _ := iostreams.Test() - - opts := &SetOptions{ - HttpClient: func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - }, - Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("owner/repo") - }, - IO: ios, - SecretName: "cool_secret", - Body: "a secret", - RandomOverride: fakeRandom, - Application: tt.opts.Application, - HasRepoOverride: tt.opts.HasRepoOverride, - Remotes: tt.opts.Remotes, - } - - err := setRun(opts) - if tt.wantErr { - assert.EqualError(t, err, tt.errMsg) - } else { - assert.NoError(t, err) - } - - reg.Verify(t) - - if tt.wantApp != "" && !tt.wantErr { - data, err := io.ReadAll(reg.Requests[1].Body) - assert.NoError(t, err) - - var payload SecretPayload - err = json.Unmarshal(data, &payload) - assert.NoError(t, err) - assert.Equal(t, payload.KeyID, "123") - assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=") - } - }) - } -} - func fakeRandom() io.Reader { return bytes.NewReader(bytes.Repeat([]byte{5}, 32)) } diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go index db2dfc8f1b1..3420f6da19f 100644 --- a/pkg/cmd/secret/shared/base_repo.go +++ b/pkg/cmd/secret/shared/base_repo.go @@ -1,47 +1,80 @@ package shared import ( - "fmt" + "errors" ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" ) -func ValidateHasOnlyOneRemote(hasRepoOverride bool, remotes func() (ghContext.Remotes, error)) error { - if !hasRepoOverride && remotes != nil { +type MultipleRemotesError struct { + Remotes ghContext.Remotes +} + +func (e MultipleRemotesError) Error() string { + return "multiple remotes detected. please specify which repo to use by providing the -R or --repo argument" +} + +type baseRepoFn func() (ghrepo.Interface, error) +type remotesFn func() (ghContext.Remotes, error) + +func PromptWhenMultipleRemotesBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Prompter) baseRepoFn { + return func() (ghrepo.Interface, error) { + baseRepo, err := baseRepoFn() + if err != nil { + var multipleRemotesError MultipleRemotesError + if !errors.As(err, &multipleRemotesError) { + return nil, err + } + + // prompt for the base repo + baseRepo, err = promptForRepo(baseRepo, multipleRemotesError.Remotes, prompter) + if err != nil { + return nil, err + } + } + + return baseRepo, nil + } +} + +// RequireNoAmbiguityBaseRepoFunc returns a function to resolve the base repo, ensuring that +// there was only one remote. +func RequireNoAmbiguityBaseRepoFunc(baseRepo baseRepoFn, remotes remotesFn) baseRepoFn { + return func() (ghrepo.Interface, error) { + // TODO: Is this really correct? Some remotes may not be in the same network. We probably need to resolve the + // network rather than looking at the remotes? remotes, err := remotes() if err != nil { - return err + return nil, err } if remotes.Len() > 1 { - return fmt.Errorf("multiple remotes detected %v. please specify which repo to use by providing the -R or --repo argument", remotes) + return nil, MultipleRemotesError{Remotes: remotes} } - } - return nil + return baseRepo() + } } -func PromptForRepo(baseRepo ghrepo.Interface, remotes func() (ghContext.Remotes, error), survey prompter.Prompter) (ghrepo.Interface, error) { +func promptForRepo(baseRepo ghrepo.Interface, remotes ghContext.Remotes, prompter prompter.Prompter) (ghrepo.Interface, error) { var defaultRepo string var remoteArray []string - if remotes, _ := remotes(); remotes != nil { - if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { - // this is a remote explicitly chosen via `repo set-default` - defaultRepo = ghrepo.FullName(defaultRemote) - } else if len(remotes) > 0 { - // as a fallback, just pick the first remote - defaultRepo = ghrepo.FullName(remotes[0]) - } + if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { + // this is a remote explicitly chosen via `repo set-default` + defaultRepo = ghrepo.FullName(defaultRemote) + } else if len(remotes) > 0 { + // as a fallback, just pick the first remote + defaultRepo = ghrepo.FullName(remotes[0]) + } - for _, remote := range remotes { - remoteArray = append(remoteArray, ghrepo.FullName(remote)) - } + for _, remote := range remotes { + remoteArray = append(remoteArray, ghrepo.FullName(remote)) } - baseRepoInput, errInput := survey.Select("Select a base repo", defaultRepo, remoteArray) + baseRepoInput, errInput := prompter.Select("Select a base repo", defaultRepo, remoteArray) if errInput != nil { return baseRepo, errInput } diff --git a/pkg/cmd/secret/shared/base_repo_test.go b/pkg/cmd/secret/shared/base_repo_test.go new file mode 100644 index 00000000000..64b2a3d550e --- /dev/null +++ b/pkg/cmd/secret/shared/base_repo_test.go @@ -0,0 +1,239 @@ +package shared_test + +import ( + "errors" + "testing" + + ghContext "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/pkg/cmd/secret/shared" + "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.MultipleRemotesError + 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() + + // Given the base repo function succeeds + baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(baseRepoStubFn, 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 resolved remote as the default", func(t *testing.T) { + t.Parallel() + + pm := prompter.NewMockPrompter(t) + pm.RegisterSelect( + "Select a base repo", + []string{"owner/fork", "owner/repo"}, + func(_, def string, opts []string) (int, error) { + t.Helper() + require.Equal(t, "owner/repo", def) + return prompter.IndexFor(opts, "owner/repo") + }, + ) + + // Given the wrapped base repo func returns a specific error + baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(errMultipleRemotesStubFn, pm) + + // When fetching the base repo + baseRepo, err := baseRepoFn() + + // 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() + + // Given the prompter returns an error + pm := prompter.NewMockPrompter(t) + pm.RegisterSelect( + "Select a base 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.PromptWhenMultipleRemotesBaseRepoFunc(errMultipleRemotesStubFn, 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() + + // Given the wrapped base repo func returns a non-specific error + baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(errBaseRepoStubFn, 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.MultipleRemotesError{} + require.EqualError(t, err, "multiple remotes detected. please specify which repo to use by providing the -R or --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", + Resolved: "base", + }, + Repo: ghrepo.New("owner", "repo"), + } + + return nil, shared.MultipleRemotesError{ + 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") +} diff --git a/pkg/cmdutil/flags.go b/pkg/cmdutil/flags.go index fadbc5fbed7..c0064099cdd 100644 --- a/pkg/cmdutil/flags.go +++ b/pkg/cmdutil/flags.go @@ -180,11 +180,3 @@ func isIncluded(value string, opts []string) bool { } return false } - -func CountSetFlags(flags *pflag.FlagSet) int { - count := 0 - flags.Visit(func(f *pflag.Flag) { - count++ - }) - return count -} From 4da4c82090d82c40ce13b43e0941d59e16181e04 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 20 Dec 2024 14:33:47 +0100 Subject: [PATCH 0209/2338] Rename secret BaseRepo func --- pkg/cmd/factory/remote_resolver.go | 1 + pkg/cmd/secret/delete/delete.go | 2 +- pkg/cmd/secret/delete/delete_test.go | 2 +- pkg/cmd/secret/list/list.go | 2 +- pkg/cmd/secret/list/list_test.go | 2 +- pkg/cmd/secret/set/set.go | 2 +- pkg/cmd/secret/set/set_test.go | 2 +- pkg/cmd/secret/shared/base_repo.go | 17 ++++++++--------- pkg/cmd/secret/shared/base_repo_test.go | 16 ++++++++-------- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/factory/remote_resolver.go b/pkg/cmd/factory/remote_resolver.go index 7e27834e2de..b008ade7e22 100644 --- a/pkg/cmd/factory/remote_resolver.go +++ b/pkg/cmd/factory/remote_resolver.go @@ -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 diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 64b02c9d964..87b96bee1cb 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -56,7 +56,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co // 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.PromptWhenMultipleRemotesBaseRepoFunc(opts.BaseRepo, f.Prompter) + opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.Prompter) } } diff --git a/pkg/cmd/secret/delete/delete_test.go b/pkg/cmd/secret/delete/delete_test.go index 91d688a8954..f1f19a591bf 100644 --- a/pkg/cmd/secret/delete/delete_test.go +++ b/pkg/cmd/secret/delete/delete_test.go @@ -156,7 +156,7 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) { { name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used", args: "SECRET_NAME", - wantErr: shared.MultipleRemotesError{ + wantErr: shared.AmbiguousBaseRepoError{ Remotes: remotes, }, }, diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 49ad47aff8f..4da9bc5693e 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -77,7 +77,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman // 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.PromptWhenMultipleRemotesBaseRepoFunc(opts.BaseRepo, f.Prompter) + opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.Prompter) } } diff --git a/pkg/cmd/secret/list/list_test.go b/pkg/cmd/secret/list/list_test.go index 63a899b7746..729984deb3b 100644 --- a/pkg/cmd/secret/list/list_test.go +++ b/pkg/cmd/secret/list/list_test.go @@ -136,7 +136,7 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) { { name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used", args: "", - wantErr: shared.MultipleRemotesError{ + wantErr: shared.AmbiguousBaseRepoError{ Remotes: remotes, }, }, diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index ba9b023a6b0..87deee71c10 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -114,7 +114,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command // 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.PromptWhenMultipleRemotesBaseRepoFunc(opts.BaseRepo, f.Prompter) + opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.Prompter) } } diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 63834521ffd..dd3124e82b5 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -255,7 +255,7 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) { { name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used", args: "SECRET_NAME", - wantErr: shared.MultipleRemotesError{ + wantErr: shared.AmbiguousBaseRepoError{ Remotes: remotes, }, }, diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go index 3420f6da19f..39a59149e5b 100644 --- a/pkg/cmd/secret/shared/base_repo.go +++ b/pkg/cmd/secret/shared/base_repo.go @@ -8,28 +8,27 @@ import ( "github.com/cli/cli/v2/internal/prompter" ) -type MultipleRemotesError struct { +type AmbiguousBaseRepoError struct { Remotes ghContext.Remotes } -func (e MultipleRemotesError) Error() string { +func (e AmbiguousBaseRepoError) Error() string { return "multiple remotes detected. please specify which repo to use by providing the -R or --repo argument" } type baseRepoFn func() (ghrepo.Interface, error) type remotesFn func() (ghContext.Remotes, error) -func PromptWhenMultipleRemotesBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Prompter) baseRepoFn { +func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Prompter) baseRepoFn { return func() (ghrepo.Interface, error) { baseRepo, err := baseRepoFn() if err != nil { - var multipleRemotesError MultipleRemotesError - if !errors.As(err, &multipleRemotesError) { + var ambiguousBaseRepoErr AmbiguousBaseRepoError + if !errors.As(err, &ambiguousBaseRepoErr) { return nil, err } - // prompt for the base repo - baseRepo, err = promptForRepo(baseRepo, multipleRemotesError.Remotes, prompter) + baseRepo, err = promptForRepo(baseRepo, ambiguousBaseRepoErr.Remotes, prompter) if err != nil { return nil, err } @@ -40,7 +39,7 @@ func PromptWhenMultipleRemotesBaseRepoFunc(baseRepoFn baseRepoFn, prompter promp } // RequireNoAmbiguityBaseRepoFunc returns a function to resolve the base repo, ensuring that -// there was only one remote. +// 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) { // TODO: Is this really correct? Some remotes may not be in the same network. We probably need to resolve the @@ -51,7 +50,7 @@ func RequireNoAmbiguityBaseRepoFunc(baseRepo baseRepoFn, remotes remotesFn) base } if remotes.Len() > 1 { - return nil, MultipleRemotesError{Remotes: remotes} + return nil, AmbiguousBaseRepoError{Remotes: remotes} } return baseRepo() diff --git a/pkg/cmd/secret/shared/base_repo_test.go b/pkg/cmd/secret/shared/base_repo_test.go index 64b2a3d550e..469a9722cef 100644 --- a/pkg/cmd/secret/shared/base_repo_test.go +++ b/pkg/cmd/secret/shared/base_repo_test.go @@ -40,7 +40,7 @@ func TestRequireNoAmbiguityBaseRepoFunc(t *testing.T) { _, err := baseRepoFn() // It succeeds and returns the inner base repo - var multipleRemotesError shared.MultipleRemotesError + var multipleRemotesError shared.AmbiguousBaseRepoError require.ErrorAs(t, err, &multipleRemotesError) require.Equal(t, ghContext.Remotes{ { @@ -92,7 +92,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { t.Parallel() // Given the base repo function succeeds - baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(baseRepoStubFn, nil) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(baseRepoStubFn, nil) // When fetching the base repo baseRepo, err := baseRepoFn() @@ -117,7 +117,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { ) // Given the wrapped base repo func returns a specific error - baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(errMultipleRemotesStubFn, pm) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, pm) // When fetching the base repo baseRepo, err := baseRepoFn() @@ -141,7 +141,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { ) // Given the wrapped base repo func returns a specific error - baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(errMultipleRemotesStubFn, pm) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, pm) // When fetching the base repo _, err := baseRepoFn() @@ -154,7 +154,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { t.Parallel() // Given the wrapped base repo func returns a non-specific error - baseRepoFn := shared.PromptWhenMultipleRemotesBaseRepoFunc(errBaseRepoStubFn, nil) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errBaseRepoStubFn, nil) // When fetching the base repo _, err := baseRepoFn() @@ -165,8 +165,8 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { } func TestMultipleRemotesErrorMessage(t *testing.T) { - err := shared.MultipleRemotesError{} - require.EqualError(t, err, "multiple remotes detected. please specify which repo to use by providing the -R or --repo argument") + 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) { @@ -185,7 +185,7 @@ func errMultipleRemotesStubFn() (ghrepo.Interface, error) { Repo: ghrepo.New("owner", "repo"), } - return nil, shared.MultipleRemotesError{ + return nil, shared.AmbiguousBaseRepoError{ Remotes: ghContext.Remotes{ remote1, remote2, From d831e3e1dbb67e81cf1cbc057f1fe6b86028b5af Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 9 Jan 2025 17:33:26 +0100 Subject: [PATCH 0210/2338] Remove validated TODO and add review warning --- pkg/cmd/secret/shared/base_repo.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go index 39a59149e5b..652d9206403 100644 --- a/pkg/cmd/secret/shared/base_repo.go +++ b/pkg/cmd/secret/shared/base_repo.go @@ -13,7 +13,7 @@ type AmbiguousBaseRepoError struct { } func (e AmbiguousBaseRepoError) Error() string { - return "multiple remotes detected. please specify which repo to use by providing the -R or --repo argument" + return "multiple remotes detected. please specify which repo to use by providing the -R, --repo argument" } type baseRepoFn func() (ghrepo.Interface, error) @@ -42,8 +42,6 @@ func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Pr // 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) { - // TODO: Is this really correct? Some remotes may not be in the same network. We probably need to resolve the - // network rather than looking at the remotes? remotes, err := remotes() if err != nil { return nil, err @@ -57,10 +55,13 @@ func RequireNoAmbiguityBaseRepoFunc(baseRepo baseRepoFn, remotes remotesFn) base } } +// REVIEW WARNING: I have not had a close look at this function yet, I do not vouch for it. func promptForRepo(baseRepo ghrepo.Interface, remotes ghContext.Remotes, prompter prompter.Prompter) (ghrepo.Interface, error) { var defaultRepo string var remoteArray []string + // TODO: consider whether we should just go with the default order of remotes because then + // users that are familiar can just hit enter and achieve the behaviour they had before. if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { // this is a remote explicitly chosen via `repo set-default` defaultRepo = ghrepo.FullName(defaultRemote) From 5630252f7861cf7b4b6bea82c10842f7e7931302 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 15 Jan 2025 13:22:10 +0100 Subject: [PATCH 0211/2338] Add acceptance test for secrets remote disambiguation --- ...secret-require-remote-disambiguation.txtar | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 acceptance/testdata/secret/secret-require-remote-disambiguation.txtar diff --git a/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar b/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar new file mode 100644 index 00000000000..f4d1bbb4a53 --- /dev/null +++ b/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar @@ -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' From ce47fabc27b1fbb1ffb275c33ce4097233dd0ddb Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 15 Jan 2025 13:55:36 +0100 Subject: [PATCH 0212/2338] Move secret base repo prompting earlier --- pkg/cmd/secret/delete/delete.go | 16 ++++++++-------- pkg/cmd/secret/set/set.go | 23 ++++++++++++----------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 87b96bee1cb..8f17fdf591f 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -99,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 @@ -108,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 diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 87deee71c10..a35c40aa3f7 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -178,23 +178,13 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command } func setRun(opts *SetOptions) error { - secrets, err := getSecretsFromOptions(opts) - if err != nil { - return err - } - - c, err := opts.HttpClient() - if err != nil { - return fmt.Errorf("could not create http client: %w", err) - } - client := api.NewClientFromHTTP(c) - 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 @@ -209,6 +199,17 @@ func setRun(opts *SetOptions) error { host, _ = cfg.Authentication().DefaultHost() } + secrets, err := getSecretsFromOptions(opts) + if err != nil { + return err + } + + c, err := opts.HttpClient() + if err != nil { + return fmt.Errorf("could not create http client: %w", err) + } + client := api.NewClientFromHTTP(c) + secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) if err != nil { return err From a47327aee6396df90349e3b61a20da3d11c2ed6c Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 15 Jan 2025 14:11:24 +0100 Subject: [PATCH 0213/2338] Secret base repo prompting should not use resolved remote This is because the secret commands don't use the SmartBaseRepo behaviour, and therefore don't care about the resolved remote. --- pkg/cmd/secret/shared/base_repo.go | 46 +++++++------------------ pkg/cmd/secret/shared/base_repo_test.go | 8 ++--- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go index 652d9206403..c467527942a 100644 --- a/pkg/cmd/secret/shared/base_repo.go +++ b/pkg/cmd/secret/shared/base_repo.go @@ -28,10 +28,22 @@ func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Pr return nil, err } - baseRepo, err = promptForRepo(baseRepo, ambiguousBaseRepoErr.Remotes, prompter) + baseRepoOptions := make([]string, len(ambiguousBaseRepoErr.Remotes)) + for i, remote := range ambiguousBaseRepoErr.Remotes { + baseRepoOptions[i] = ghrepo.FullName(remote) + } + + selectedBaseRepo, err := prompter.Select("Select a base 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 @@ -54,35 +66,3 @@ func RequireNoAmbiguityBaseRepoFunc(baseRepo baseRepoFn, remotes remotesFn) base return baseRepo() } } - -// REVIEW WARNING: I have not had a close look at this function yet, I do not vouch for it. -func promptForRepo(baseRepo ghrepo.Interface, remotes ghContext.Remotes, prompter prompter.Prompter) (ghrepo.Interface, error) { - var defaultRepo string - var remoteArray []string - - // TODO: consider whether we should just go with the default order of remotes because then - // users that are familiar can just hit enter and achieve the behaviour they had before. - if defaultRemote, _ := remotes.ResolvedRemote(); defaultRemote != nil { - // this is a remote explicitly chosen via `repo set-default` - defaultRepo = ghrepo.FullName(defaultRemote) - } else if len(remotes) > 0 { - // as a fallback, just pick the first remote - defaultRepo = ghrepo.FullName(remotes[0]) - } - - for _, remote := range remotes { - remoteArray = append(remoteArray, ghrepo.FullName(remote)) - } - - baseRepoInput, errInput := prompter.Select("Select a base repo", defaultRepo, remoteArray) - if errInput != nil { - return baseRepo, errInput - } - - selectedRepo, errSelectedRepo := ghrepo.FromFullName(remoteArray[baseRepoInput]) - if errSelectedRepo != nil { - return baseRepo, errSelectedRepo - } - - return selectedRepo, nil -} diff --git a/pkg/cmd/secret/shared/base_repo_test.go b/pkg/cmd/secret/shared/base_repo_test.go index 469a9722cef..d5cdc5e2352 100644 --- a/pkg/cmd/secret/shared/base_repo_test.go +++ b/pkg/cmd/secret/shared/base_repo_test.go @@ -102,7 +102,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { 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 resolved remote as the default", func(t *testing.T) { + 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() pm := prompter.NewMockPrompter(t) @@ -110,8 +110,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { "Select a base repo", []string{"owner/fork", "owner/repo"}, func(_, def string, opts []string) (int, error) { - t.Helper() - require.Equal(t, "owner/repo", def) + require.Equal(t, "owner/fork", def) return prompter.IndexFor(opts, "owner/repo") }, ) @@ -179,8 +178,7 @@ func errMultipleRemotesStubFn() (ghrepo.Interface, error) { remote2 := &ghContext.Remote{ Remote: &git.Remote{ - Name: "upstream", - Resolved: "base", + Name: "upstream", }, Repo: ghrepo.New("owner", "repo"), } From b382b2472823a929117c751cbbfdf20e4c232c61 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 15 Jan 2025 14:22:43 +0100 Subject: [PATCH 0214/2338] Print informative message before prompting for secret repo --- pkg/cmd/secret/delete/delete.go | 2 +- pkg/cmd/secret/list/list.go | 2 +- pkg/cmd/secret/set/set.go | 2 +- pkg/cmd/secret/shared/base_repo.go | 5 ++++- pkg/cmd/secret/shared/base_repo_test.go | 22 +++++++++++++++++----- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 8f17fdf591f..78550f92864 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -56,7 +56,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co // 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.Prompter) + opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.IOStreams, f.Prompter) } } diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 4da9bc5693e..41cf88e642d 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -77,7 +77,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman // 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.Prompter) + opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.IOStreams, f.Prompter) } } diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index a35c40aa3f7..ba90e1f6797 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -114,7 +114,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command // 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.Prompter) + opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.IOStreams, f.Prompter) } } diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go index c467527942a..d902105aab1 100644 --- a/pkg/cmd/secret/shared/base_repo.go +++ b/pkg/cmd/secret/shared/base_repo.go @@ -2,10 +2,12 @@ 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 { @@ -19,7 +21,7 @@ func (e AmbiguousBaseRepoError) Error() string { type baseRepoFn func() (ghrepo.Interface, error) type remotesFn func() (ghContext.Remotes, error) -func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Prompter) baseRepoFn { +func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, ios *iostreams.IOStreams, prompter prompter.Prompter) baseRepoFn { return func() (ghrepo.Interface, error) { baseRepo, err := baseRepoFn() if err != nil { @@ -33,6 +35,7 @@ func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, prompter prompter.Pr 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 base repo", baseRepoOptions[0], baseRepoOptions) if err != nil { return nil, err diff --git a/pkg/cmd/secret/shared/base_repo_test.go b/pkg/cmd/secret/shared/base_repo_test.go index d5cdc5e2352..6b7aa02ff5d 100644 --- a/pkg/cmd/secret/shared/base_repo_test.go +++ b/pkg/cmd/secret/shared/base_repo_test.go @@ -6,6 +6,7 @@ import ( 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" @@ -91,8 +92,10 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { 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, nil) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(baseRepoStubFn, ios, nil) // When fetching the base repo baseRepo, err := baseRepoFn() @@ -105,6 +108,8 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { 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 base repo", @@ -116,12 +121,15 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { ) // Given the wrapped base repo func returns a specific error - baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, pm) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, ios, pm) // When fetching the base repo baseRepo, err := baseRepoFn() - // It uses the prompter for disambiguation + // 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)) }) @@ -129,6 +137,8 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { 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( @@ -140,7 +150,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { ) // Given the wrapped base repo func returns a specific error - baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, pm) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, ios, pm) // When fetching the base repo _, err := baseRepoFn() @@ -152,8 +162,10 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { 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, nil) + baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errBaseRepoStubFn, ios, nil) // When fetching the base repo _, err := baseRepoFn() From 88a64d2a1135db1a3cfa508d43ebdb9c9b1cf4f8 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 15 Jan 2025 14:31:32 +0100 Subject: [PATCH 0215/2338] Change wording on secret repo prompt --- pkg/cmd/secret/delete/delete_test.go | 2 +- pkg/cmd/secret/list/list_test.go | 2 +- pkg/cmd/secret/set/set_test.go | 2 +- pkg/cmd/secret/shared/base_repo.go | 2 +- pkg/cmd/secret/shared/base_repo_test.go | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/secret/delete/delete_test.go b/pkg/cmd/secret/delete/delete_test.go index f1f19a591bf..377879cf2ab 100644 --- a/pkg/cmd/secret/delete/delete_test.go +++ b/pkg/cmd/secret/delete/delete_test.go @@ -165,7 +165,7 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) { args: "SECRET_NAME", prompterStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect( - "Select a base repo", + "Select a repo", []string{"owner/fork", "owner/repo"}, func(_, _ string, opts []string) (int, error) { return prompter.IndexFor(opts, "owner/fork") diff --git a/pkg/cmd/secret/list/list_test.go b/pkg/cmd/secret/list/list_test.go index 729984deb3b..601de2572ff 100644 --- a/pkg/cmd/secret/list/list_test.go +++ b/pkg/cmd/secret/list/list_test.go @@ -145,7 +145,7 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) { args: "", prompterStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect( - "Select a base repo", + "Select a repo", []string{"owner/fork", "owner/repo"}, func(_, _ string, opts []string) (int, error) { return prompter.IndexFor(opts, "owner/fork") diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index dd3124e82b5..99c8c6cbe3b 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -264,7 +264,7 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) { args: "SECRET_NAME", prompterStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect( - "Select a base repo", + "Select a repo", []string{"owner/fork", "owner/repo"}, func(_, _ string, opts []string) (int, error) { return prompter.IndexFor(opts, "owner/fork") diff --git a/pkg/cmd/secret/shared/base_repo.go b/pkg/cmd/secret/shared/base_repo.go index d902105aab1..308117ac6cb 100644 --- a/pkg/cmd/secret/shared/base_repo.go +++ b/pkg/cmd/secret/shared/base_repo.go @@ -36,7 +36,7 @@ func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, ios *iostreams.IOStr } 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 base repo", baseRepoOptions[0], baseRepoOptions) + selectedBaseRepo, err := prompter.Select("Select a repo", baseRepoOptions[0], baseRepoOptions) if err != nil { return nil, err } diff --git a/pkg/cmd/secret/shared/base_repo_test.go b/pkg/cmd/secret/shared/base_repo_test.go index 6b7aa02ff5d..d78fad7e5b4 100644 --- a/pkg/cmd/secret/shared/base_repo_test.go +++ b/pkg/cmd/secret/shared/base_repo_test.go @@ -112,7 +112,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { pm := prompter.NewMockPrompter(t) pm.RegisterSelect( - "Select a base repo", + "Select a repo", []string{"owner/fork", "owner/repo"}, func(_, def string, opts []string) (int, error) { require.Equal(t, "owner/fork", def) @@ -142,7 +142,7 @@ func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) { // Given the prompter returns an error pm := prompter.NewMockPrompter(t) pm.RegisterSelect( - "Select a base repo", + "Select a repo", []string{"owner/fork", "owner/repo"}, func(_, _ string, opts []string) (int, error) { return 0, errors.New("test prompt error") From e7ffb1e435e4faa081af3c6638f7f648e94f94f0 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 15 Jan 2025 14:38:05 +0100 Subject: [PATCH 0216/2338] Fix typo in secret base repo selection comment --- pkg/cmd/secret/delete/delete.go | 2 +- pkg/cmd/secret/list/list.go | 2 +- pkg/cmd/secret/set/set.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/secret/delete/delete.go b/pkg/cmd/secret/delete/delete.go index 78550f92864..84a5e7a8304 100644 --- a/pkg/cmd/secret/delete/delete.go +++ b/pkg/cmd/secret/delete/delete.go @@ -50,7 +50,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co // 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 error if + // 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 diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 41cf88e642d..af3526386ed 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -71,7 +71,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman // 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 error if + // 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 diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index ba90e1f6797..3c12b41924e 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -108,7 +108,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command // 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 error if + // 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 From 2757d22b4b236ad0d24104a5027d6ec87cc1e657 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:49:31 +0000 Subject: [PATCH 0217/2338] Bump google.golang.org/protobuf from 1.36.2 to 1.36.3 Bumps google.golang.org/protobuf from 1.36.2 to 1.36.3. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0921a544fc2..910ab484d53 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( golang.org/x/term v0.27.0 golang.org/x/text v0.21.0 google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.36.2 + google.golang.org/protobuf v1.36.3 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 73158e4e06a..4314215dbcf 100644 --- a/go.sum +++ b/go.sum @@ -548,8 +548,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.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= From 2ffce8ae9fbccfa8e6f1d6d5ec7061cadc7da228 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 15 Jan 2025 14:11:54 -0700 Subject: [PATCH 0218/2338] print attestation output info as bullet points instead of table Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/io/handler.go | 18 +++++++++++++++++- pkg/cmd/attestation/verify/verify.go | 25 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go index 167128a4244..1e5114040b0 100644 --- a/pkg/cmd/attestation/io/handler.go +++ b/pkg/cmd/attestation/io/handler.go @@ -2,6 +2,7 @@ package io import ( "fmt" + "strings" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/iostreams" @@ -65,10 +66,25 @@ 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) PrintBulletPoints(rows [][]string) (int, error) { + maxColLen := 0 + for _, row := range rows { + if len(row[0]) > maxColLen { + maxColLen = len(row[0]) + } + } + + 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 fmt.Fprintln(h.IO.ErrOut, info) +} + func (h *Handler) PrintTable(headers []string, rows [][]string) error { if !h.IO.IsStdoutTTY() { return nil diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 7b6250c0c47..73689a1b8fb 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -262,21 +262,30 @@ func runVerify(opts *Options) error { return nil } - opts.Logger.Printf("The following %d %s matched the policy criteria:\n\n", len(verified), text.Pluralize(len(verified), "attestation")) + opts.Logger.Printf("%s matched the policy criteria:\n", text.Pluralize(len(verified), "attestation")) - // Otherwise print the results to the terminal in a table - tableContent, err := buildTableVerifyContent(opts.Tenant, verified) + // Otherwise print the results to the terminal + buildConfigURI := verified[0].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 results")) + opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse build config URI")) return err } - - headers := []string{"signer repo", "signer workflow", "builder repo", "builder workflow"} - if err = opts.Logger.PrintTable(headers, tableContent); err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to print attestation details to table")) + builderSignerURI := verified[0].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 } + rows := [][]string{ + []string{"- Build repo", sourceRepoAndOrg}, + []string{"- Build workflow", sourceWorkflow}, + []string{"- Signer repo", signerRepoAndOrg}, + []string{"- Signer workflow", signerWorkflow}, + } + opts.Logger.PrintBulletPoints(rows) + // All attestations passed verification and policy evaluation return nil } From 555c9b65f56f4b7681fcaf12a14ec3544027da00 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 15 Jan 2025 14:13:16 -0700 Subject: [PATCH 0219/2338] drop unused handler method Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/io/handler.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go index 1e5114040b0..3cbfc656ae7 100644 --- a/pkg/cmd/attestation/io/handler.go +++ b/pkg/cmd/attestation/io/handler.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/utils" ) @@ -84,23 +83,3 @@ func (h *Handler) PrintBulletPoints(rows [][]string) (int, error) { } return fmt.Fprintln(h.IO.ErrOut, info) } - -func (h *Handler) PrintTable(headers []string, rows [][]string) error { - if !h.IO.IsStdoutTTY() { - return nil - } - - t := tableprinter.New(h.IO, tableprinter.WithHeader(headers...)) - - for _, row := range rows { - for _, field := range row { - t.AddField(field, tableprinter.WithTruncate(nil)) - } - t.EndRow() - } - - if err := t.Render(); err != nil { - return fmt.Errorf("failed to print output: %v", err) - } - return nil -} From 6d2f71d0f4bfb4e5c78037237f5d06ae1e4b7670 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 15 Jan 2025 14:17:49 -0700 Subject: [PATCH 0220/2338] delete unused function Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 73689a1b8fb..ff1d34b5a0e 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -326,30 +326,3 @@ func extractAttestationDetail(tenant, builderSignerURI string) (string, string, 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 - buildRepoAndOrg, buildWorkflow, err := extractAttestationDetail(tenant, builderSignerURI) - if err != nil { - return nil, err - } - - buildConfigURI := res.VerificationResult.Signature.Certificate.Extensions.BuildConfigURI - sourceRepoAndOrg, sourceWorkflow, err := extractAttestationDetail(tenant, buildConfigURI) - if err != nil { - return nil, err - } - - content[i] = []string{buildRepoAndOrg, buildWorkflow, sourceRepoAndOrg, sourceWorkflow} - } - - return content, nil -} From 4fb265c5970091df04e3230b7823278fdf46a0b3 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 15 Jan 2025 14:22:05 -0700 Subject: [PATCH 0221/2338] formatting Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index ff1d34b5a0e..837fcb72763 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -279,10 +279,10 @@ func runVerify(opts *Options) error { } rows := [][]string{ - []string{"- Build repo", sourceRepoAndOrg}, - []string{"- Build workflow", sourceWorkflow}, - []string{"- Signer repo", signerRepoAndOrg}, - []string{"- Signer workflow", signerWorkflow}, + {"- Build repo", sourceRepoAndOrg}, + {"- Build workflow", sourceWorkflow}, + {"- Signer repo", signerRepoAndOrg}, + {"- Signer workflow", signerWorkflow}, } opts.Logger.PrintBulletPoints(rows) From daf63543ea92c0e8e9d64303f6d9bd67817a5862 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 15 Jan 2025 14:33:13 -0700 Subject: [PATCH 0222/2338] add tty check Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/io/handler.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go index 3cbfc656ae7..06d747bf40e 100644 --- a/pkg/cmd/attestation/io/handler.go +++ b/pkg/cmd/attestation/io/handler.go @@ -69,6 +69,9 @@ func (h *Handler) VerbosePrintf(f string, v ...interface{}) (int, error) { } func (h *Handler) PrintBulletPoints(rows [][]string) (int, error) { + if !h.IO.IsStdoutTTY() { + return 0, nil + } maxColLen := 0 for _, row := range rows { if len(row[0]) > maxColLen { From 5d26f0b767a98faf3b4b0962dbb7ce50a9ca472b Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 15 Jan 2025 14:39:22 -0700 Subject: [PATCH 0223/2338] skip check Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 837fcb72763..9c285e5c3ee 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -284,6 +284,7 @@ func runVerify(opts *Options) error { {"- Signer repo", signerRepoAndOrg}, {"- Signer workflow", signerWorkflow}, } + //nolint:errcheck opts.Logger.PrintBulletPoints(rows) // All attestations passed verification and policy evaluation From 4a9fd9508f71abf4dd81d61a3e6b38088a9f775c Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 15 Jan 2025 16:13:03 -0800 Subject: [PATCH 0224/2338] Add comments and a bit of code cleanup --- git/client.go | 34 +++++++---- git/client_test.go | 97 +++++++++++++------------------- git/objects.go | 12 ++-- pkg/cmd/pr/status/status.go | 3 +- pkg/cmd/pr/status/status_test.go | 8 +-- 5 files changed, 73 insertions(+), 81 deletions(-) diff --git a/git/client.go b/git/client.go index 82ead58a487..956d1367158 100644 --- a/git/client.go +++ b/git/client.go @@ -402,23 +402,25 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon return BranchConfig{}, nil } - pushDefaultOut, err := c.Config(ctx, "remote.pushDefault") + // Check to see if there is a pushDefault ref set for the repo + remotePushDefaultOut, err := c.Config(ctx, "remote.pushDefault") if ok := errors.As(err, &gitError); ok && gitError.ExitCode != 1 { return BranchConfig{}, err } - revParseOut, err := c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}") - if ok := errors.As(err, &gitError); ok && gitError.ExitCode != 1 { - return BranchConfig{}, err - } + // Check to see if we can resolve the @{push} revision syntax. This is the easiest way to get + // the name of the push remote. + //We ignore errors resolving simple push.Default settings as these are handled downstream + revParseOut, _ := c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}") - return parseBranchConfig(outputLines(branchCfgOut), strings.TrimSuffix(pushDefaultOut, "\n"), firstLine(revParseOut)), nil + return parseBranchConfig(outputLines(branchCfgOut), strings.TrimSuffix(remotePushDefaultOut, "\n"), firstLine(revParseOut)), nil } -func parseBranchConfig(configLines []string, pushDefault string, revParse string) BranchConfig { +func parseBranchConfig(branchConfigLines []string, remotePushDefault string, revParse string) BranchConfig { var cfg BranchConfig - for _, line := range configLines { + // Read the config lines for the specific branch + for _, line := range branchConfigLines { parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { continue @@ -429,6 +431,7 @@ func parseBranchConfig(configLines []string, pushDefault string, revParse string remoteURL, remoteName := parseRemoteURLOrName(parts[1]) cfg.RemoteURL = remoteURL cfg.RemoteName = remoteName + // pushremote usually indicates a "triangular" workflow case "pushremote": pushRemoteURL, pushRemoteName := parseRemoteURLOrName(parts[1]) cfg.PushRemoteURL = pushRemoteURL @@ -440,18 +443,27 @@ func parseBranchConfig(configLines []string, pushDefault string, revParse string } } + // PushRemote{URL|Name} takes precedence over remotePushDefault, so we'll only + // use remotePushDefault if we don't have a push remote. if cfg.PushRemoteURL == nil && cfg.PushRemoteName == "" { - if pushDefault != "" { - pushRemoteURL, pushRemoteName := parseRemoteURLOrName(pushDefault) + // remotePushDefault usually indicates a "triangular" workflow + if remotePushDefault != "" { + pushRemoteURL, pushRemoteName := parseRemoteURLOrName(remotePushDefault) cfg.PushRemoteURL = pushRemoteURL cfg.PushRemoteName = pushRemoteName } else { + // Without a PushRemote{URL|Name} or a remotePushDefault, we assume that the + // push remote ref is the same as the remote ref. This is likely + // a "centralized" workflow. cfg.PushRemoteName = cfg.RemoteName } } + // The value returned by revParse is the easiest way to get the name of the push ref cfg.Push = revParse - cfg.PushDefaultName = pushDefault + + // Some `gh pr` workflows don't work if this is set to 'simple' or 'current' + cfg.RemotePushDefault = remotePushDefault return cfg } diff --git a/git/client_test.go b/git/client_test.go index 801d7c0255a..ba13c889ac3 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -787,7 +787,7 @@ func TestClientReadBranchConfig(t *testing.T) { Stdout: "branch.trunk.remote origin\n", }, `path/to/git config remote.pushDefault`: { - Stdout: "pushdefault", + Stdout: "remotePushDefault", }, `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { Stdout: "origin/trunk", @@ -795,10 +795,10 @@ func TestClientReadBranchConfig(t *testing.T) { }, branch: "trunk", wantBranchConfig: BranchConfig{ - RemoteName: "origin", - PushRemoteName: "pushdefault", - Push: "origin/trunk", - PushDefaultName: "pushdefault", + RemoteName: "origin", + PushRemoteName: "remotePushDefault", + Push: "origin/trunk", + RemotePushDefault: "remotePushDefault", }, wantError: nil, }, @@ -809,7 +809,7 @@ func TestClientReadBranchConfig(t *testing.T) { Stdout: "branch.trunk.remote origin\n", }, `path/to/git config remote.pushDefault`: { - Stdout: "pushdefault", + Stdout: "remotePushDefault", }, `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { ExitStatus: 1, @@ -817,49 +817,28 @@ func TestClientReadBranchConfig(t *testing.T) { }, branch: "trunk", wantBranchConfig: BranchConfig{ - RemoteName: "origin", - PushRemoteName: "pushdefault", - PushDefaultName: "pushdefault", + RemoteName: "origin", + PushRemoteName: "remotePushDefault", + RemotePushDefault: "remotePushDefault", }, wantError: nil, }, { - name: "when git reads the config, pushDefault is set, and rev-parse fails, it should return an empty BranchConfig and the error", + name: "when git reads the config but remotePushDefault fails, it should return and empty BranchConfig and the error", cmds: mockedCommands{ `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { Stdout: "branch.trunk.remote origin\n", }, `path/to/git config remote.pushDefault`: { - Stdout: "pushdefault", - }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { ExitStatus: 2, - Stderr: "rev-parse error", + Stderr: "remotePushDefault error", }, }, branch: "trunk", wantBranchConfig: BranchConfig{}, wantError: &GitError{ ExitCode: 2, - Stderr: "rev-parse error", - }, - }, - { - name: "when git reads the config but pushdefault fails, it should return and empty BranchConfig and the error", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\n", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 2, - Stderr: "pushdefault error", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{}, - wantError: &GitError{ - ExitCode: 2, - Stderr: "pushdefault error", + Stderr: "remotePushDefault error", }, }, { @@ -963,11 +942,11 @@ func TestClientReadBranchConfig(t *testing.T) { }, branch: "trunk", wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - Push: "origin/trunk", - PushDefaultName: "origin", + RemoteName: "upstream", + MergeRef: "refs/heads/main", + PushRemoteName: "origin", + Push: "origin/trunk", + RemotePushDefault: "origin", }, }, { @@ -985,10 +964,10 @@ func TestClientReadBranchConfig(t *testing.T) { }, branch: "trunk", wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - PushDefaultName: "current", + RemoteName: "upstream", + MergeRef: "refs/heads/main", + PushRemoteName: "origin", + RemotePushDefault: "current", }, }, { @@ -1006,10 +985,10 @@ func TestClientReadBranchConfig(t *testing.T) { }, branch: "trunk", wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - PushDefaultName: "origin", + RemoteName: "upstream", + MergeRef: "refs/heads/main", + PushRemoteName: "origin", + RemotePushDefault: "origin", }, }, } @@ -1021,15 +1000,15 @@ func TestClientReadBranchConfig(t *testing.T) { commandContext: cmdCtx, } branchConfig, err := client.ReadBranchConfig(context.Background(), tt.branch) - assert.Equal(t, tt.wantBranchConfig, branchConfig) if tt.wantError != nil { var gitError *GitError require.ErrorAs(t, err, &gitError) assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode) assert.Equal(t, tt.wantError.Stderr, gitError.Stderr) } else { - assert.NoError(t, err) + require.NoError(t, err) } + assert.Equal(t, tt.wantBranchConfig, branchConfig) }) } } @@ -1082,10 +1061,10 @@ func Test_parseBranchConfig(t *testing.T) { { name: "push default specified", configLines: []string{}, - pushDefault: "pushdefault", + pushDefault: "remotePushDefault", wantBranchConfig: BranchConfig{ - PushRemoteName: "pushdefault", - PushDefaultName: "pushdefault", + PushRemoteName: "remotePushDefault", + RemotePushDefault: "remotePushDefault", }, }, { @@ -1128,15 +1107,15 @@ func Test_parseBranchConfig(t *testing.T) { "branch.trunk.gh-merge-base gh-merge-base", "branch.trunk.merge refs/heads/trunk", }, - pushDefault: "pushdefault", + pushDefault: "remotePushDefault", revParse: "origin/trunk", wantBranchConfig: BranchConfig{ - RemoteName: "remote", - PushRemoteName: "pushremote", - MergeBase: "gh-merge-base", - MergeRef: "refs/heads/trunk", - Push: "origin/trunk", - PushDefaultName: "pushdefault", + RemoteName: "remote", + PushRemoteName: "pushremote", + MergeBase: "gh-merge-base", + MergeRef: "refs/heads/trunk", + Push: "origin/trunk", + RemotePushDefault: "remotePushDefault", }, }, { @@ -1158,7 +1137,7 @@ func Test_parseBranchConfig(t *testing.T) { assert.Equalf(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase, "unexpected MergeBase") assert.Equalf(t, tt.wantBranchConfig.PushRemoteName, branchConfig.PushRemoteName, "unexpected PushRemoteName") assert.Equalf(t, tt.wantBranchConfig.Push, branchConfig.Push, "unexpected Push") - assert.Equalf(t, tt.wantBranchConfig.PushDefaultName, branchConfig.PushDefaultName, "unexpected PushDefaultName") + assert.Equalf(t, tt.wantBranchConfig.RemotePushDefault, branchConfig.RemotePushDefault, "unexpected RemotePushDefault") if tt.wantBranchConfig.RemoteURL != nil { assert.Equalf(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String(), "unexpected RemoteURL") } diff --git a/git/objects.go b/git/objects.go index 8a9d086c66c..ff4068173b9 100644 --- a/git/objects.go +++ b/git/objects.go @@ -64,10 +64,10 @@ type BranchConfig struct { RemoteName string RemoteURL *url.URL // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. - MergeBase string - MergeRef string - PushDefaultName string - PushRemoteURL *url.URL - PushRemoteName string - Push string + MergeBase string + MergeRef string + RemotePushDefault string + PushRemoteURL *url.URL + PushRemoteName string + Push string } diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 593f05e6337..fe674ad6ee7 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -226,8 +226,9 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I if branchOwner != "" { selector := prHeadRef if branchConfig.Push != "" { + // If we have a resolved push revision ref, we defer to that selector = strings.TrimPrefix(branchConfig.Push, branchConfig.PushRemoteName+"/") - } else if (branchConfig.PushDefaultName == "upstream" || branchConfig.PushDefaultName == "tracking") && + } else if (branchConfig.RemotePushDefault == "upstream" || branchConfig.RemotePushDefault == "tracking") && strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 97524acbaa4..586e221837c 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -666,8 +666,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { Host: "github.com", Path: "Frederick888/playground.git", }, - MergeRef: "refs/heads/main", - PushDefaultName: "upstream", + MergeRef: "refs/heads/main", + RemotePushDefault: "upstream", }, baseRepo: ghrepo.NewWithHost("octocat", "playground", "github.com"), prHeadRef: "Frederick888/main", @@ -690,8 +690,8 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { Host: "github.com", Path: "Frederick888/playground.git", }, - MergeRef: "refs/heads/main", - PushDefaultName: "tracking", + MergeRef: "refs/heads/main", + RemotePushDefault: "tracking", }, baseRepo: ghrepo.NewWithHost("octocat", "playground", "github.com"), prHeadRef: "Frederick888/main", From d289ddd6178aa177f9c564dfc9b299c642b50cff Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 15 Jan 2025 16:21:06 -0800 Subject: [PATCH 0225/2338] Use PushRemoteURL instead of RemoteURL in prSelectorForCurrentBranch --- git/client.go | 1 + pkg/cmd/pr/status/status.go | 5 ++--- pkg/cmd/pr/status/status_test.go | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/git/client.go b/git/client.go index 956d1367158..5e47098c20d 100644 --- a/git/client.go +++ b/git/client.go @@ -456,6 +456,7 @@ func parseBranchConfig(branchConfigLines []string, remotePushDefault string, rev // push remote ref is the same as the remote ref. This is likely // a "centralized" workflow. cfg.PushRemoteName = cfg.RemoteName + cfg.PushRemoteURL = cfg.RemoteURL } } diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index fe674ad6ee7..89926bfa222 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -199,11 +199,10 @@ func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.I } return prNumber, prHeadRef, nil } - var branchOwner string - if branchConfig.RemoteURL != nil { + if branchConfig.PushRemoteURL != nil { // the branch merges from a remote specified by URL - r, err := ghrepo.FromURL(branchConfig.RemoteURL) + r, err := ghrepo.FromURL(branchConfig.PushRemoteURL) 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 diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 586e221837c..4e9ca19b0ea 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -494,7 +494,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "Branch merges from a remote specified by URL", branchConfig: git.BranchConfig{ - RemoteURL: &url.URL{ + PushRemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", @@ -537,7 +537,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "Branch is a fork and merges from a remote specified by URL", branchConfig: git.BranchConfig{ - RemoteURL: &url.URL{ + PushRemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", @@ -625,7 +625,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "Remote URL errors", branchConfig: git.BranchConfig{ - RemoteURL: &url.URL{ + PushRemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", @@ -660,7 +660,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "Current branch pushes to default upstream", branchConfig: git.BranchConfig{ - RemoteURL: &url.URL{ + PushRemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", @@ -684,7 +684,7 @@ func Test_prSelectorForCurrentBranch(t *testing.T) { { name: "Current branch pushes to default tracking", branchConfig: git.BranchConfig{ - RemoteURL: &url.URL{ + PushRemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", From aef2642581348e56b6031087288c40aae268cc8b Mon Sep 17 00:00:00 2001 From: Frederick Zhang Date: Thu, 16 Jan 2025 15:07:33 +1100 Subject: [PATCH 0226/2338] fixup! Add comments and a bit of code cleanup --- git/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/client.go b/git/client.go index 5e47098c20d..9ef6c152ebb 100644 --- a/git/client.go +++ b/git/client.go @@ -410,7 +410,7 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon // Check to see if we can resolve the @{push} revision syntax. This is the easiest way to get // the name of the push remote. - //We ignore errors resolving simple push.Default settings as these are handled downstream + // We ignore errors resolving simple push.Default settings as these are handled downstream revParseOut, _ := c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}") return parseBranchConfig(outputLines(branchCfgOut), strings.TrimSuffix(remotePushDefaultOut, "\n"), firstLine(revParseOut)), nil From 243acaf5792d62234f769c2a9125f81616cd6328 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Thu, 16 Jan 2025 00:08:45 -0500 Subject: [PATCH 0227/2338] Refactor test based on PR feedback --- pkg/cmd/root/extension_test.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/root/extension_test.go b/pkg/cmd/root/extension_test.go index 7dd66df3101..8246c0e2ac1 100644 --- a/pkg/cmd/root/extension_test.go +++ b/pkg/cmd/root/extension_test.go @@ -1,6 +1,7 @@ package root_test import ( + "fmt" "io" "testing" "time" @@ -122,8 +123,6 @@ 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 1 second to allow update checking logic to complete. - time.Sleep(1 * time.Second) return true, nil }, } @@ -174,7 +173,7 @@ func TestNewCmdExtension_Updates(t *testing.T) { } func TestNewCmdExtension_UpdateCheckIsNonblocking(t *testing.T) { - ios, _, _, stderr := iostreams.Test() + ios, _, _, _ := iostreams.Test() em := &extensions.ExtensionManagerMock{ DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { @@ -204,19 +203,30 @@ func TestNewCmdExtension_UpdateCheckIsNonblocking(t *testing.T) { }, } + // 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) { - // This function runs in the background as a goroutine while the extension is dispatched. - // Testing whether the extension cobra command is non-blocking for extension update check, - // this function will cause goroutine to sleep for a sufficiently long time before panicking. - // The idea is that the test will conclude because the dispatch function completes immediately - // and exits before the check update function completes. time.Sleep(30 * time.Second) - panic("It seems like the extension cobra command might be blocking on update checking when it should not block on long update checks") + return nil, fmt.Errorf("update check should not have completed") } cmd := root.NewCmdExtension(ios, em, ext, checkFunc) - _, err := cmd.ExecuteC() - require.NoError(t, err) - require.Emptyf(t, stderr.String(), "long running, non-blocking extension update check should not have displayed anything on stderr") + // 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") + } } From cc4bf0fc9fc58da7f4eb522aad3fa41f0f0068f5 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 16 Jan 2025 14:10:42 +0100 Subject: [PATCH 0228/2338] Add small wait to extension update tests --- pkg/cmd/root/extension_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/root/extension_test.go b/pkg/cmd/root/extension_test.go index 8246c0e2ac1..5e9e9b9bcf5 100644 --- a/pkg/cmd/root/extension_test.go +++ b/pkg/cmd/root/extension_test.go @@ -123,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 }, } From 35d81e3ff596071005ce07d39c0e8e5a4d09fb50 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:03:48 -0700 Subject: [PATCH 0229/2338] Fix: Gist edit tests for interactivity This changes the gist edit tests to use the positive `istty` instead of the previous inverse `nontty`, which is consistent with the way other commands are written. --- pkg/cmd/gist/edit/edit_test.go | 223 ++++++++++++++++++++++----------- 1 file changed, 148 insertions(+), 75 deletions(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index be610b6b475..a40695c39d1 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -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", + 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{}{ @@ -520,8 +550,8 @@ func Test_editRun(t *testing.T) { }, }, { - name: "no arguments notty", - nontty: true, + name: "no arguments notty", + istty: false, opts: &EditOptions{ Selector: "", }, @@ -531,24 +561,62 @@ func Test_editRun(t *testing.T) { for _, tt := range tests { reg := &httpmock.Registry{} - if tt.gist == nil { - reg.Register(httpmock.REST("GET", "gists/1234"), - httpmock.StatusStringResponse(404, "Not Found")) - } else { + 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.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 } @@ -558,19 +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.SetStderrTTY(!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) } @@ -584,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()) From dec46670bb356a214e0ae4ecc8fd689a55df5214 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:14:33 -0700 Subject: [PATCH 0230/2338] Fix: gist edit/view tests var name consistency --- pkg/cmd/gist/edit/edit_test.go | 16 ++++++++-------- pkg/cmd/gist/view/view_test.go | 28 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index a40695c39d1..5a3f0a2686b 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -149,7 +149,7 @@ func Test_editRun(t *testing.T) { mockGistList bool httpStubs func(*httpmock.Registry) prompterStubs func(*prompter.MockPrompter) - istty bool + isTTY bool stdin string wantErr string wantLastRequestParameters map[string]interface{} @@ -163,7 +163,7 @@ func Test_editRun(t *testing.T) { }, { name: "one file", - istty: false, + isTTY: false, opts: &EditOptions{ Selector: "1234", }, @@ -194,7 +194,7 @@ func Test_editRun(t *testing.T) { }, { name: "multiple files, submit, with TTY", - istty: true, + isTTY: true, mockGistList: true, prompterStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Edit which file?", @@ -243,7 +243,7 @@ func Test_editRun(t *testing.T) { }, { name: "multiple files, cancel", - istty: true, + isTTY: true, opts: &EditOptions{ Selector: "1234", }, @@ -551,7 +551,7 @@ func Test_editRun(t *testing.T) { }, { name: "no arguments notty", - istty: false, + isTTY: false, opts: &EditOptions{ Selector: "", }, @@ -626,9 +626,9 @@ func Test_editRun(t *testing.T) { } ios, stdin, stdout, stderr := iostreams.Test() stdin.WriteString(tt.stdin) - ios.SetStdoutTTY(tt.istty) - ios.SetStdinTTY(tt.istty) - ios.SetStderrTTY(tt.istty) + ios.SetStdoutTTY(tt.isTTY) + ios.SetStdinTTY(tt.isTTY) + ios.SetStderrTTY(tt.isTTY) tt.opts.IO = ios diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 820401f1e38..706b85f103e 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -116,10 +116,10 @@ func Test_viewRun(t *testing.T) { name string opts *ViewOptions wantOut string - gist *shared.Gist + mockGist *shared.Gist + mockGistList bool isTTY bool wantErr string - mockGistList bool }{ { name: "no such gist", @@ -137,7 +137,7 @@ func Test_viewRun(t *testing.T) { Selector: "1234", ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Files: map[string]*shared.GistFile{ "cicada.txt": { Content: "bwhiizzzbwhuiiizzzz", @@ -155,7 +155,7 @@ func Test_viewRun(t *testing.T) { ListFiles: false, }, mockGistList: true, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Files: map[string]*shared.GistFile{ "cicada.txt": { Content: "test interactive mode", @@ -178,7 +178,7 @@ func Test_viewRun(t *testing.T) { Filename: "cicada.txt", ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Files: map[string]*shared.GistFile{ "cicada.txt": { Content: "bwhiizzzbwhuiiizzzz", @@ -201,7 +201,7 @@ func Test_viewRun(t *testing.T) { Raw: true, ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Files: map[string]*shared.GistFile{ "cicada.txt": { Content: "bwhiizzzbwhuiiizzzz", @@ -222,7 +222,7 @@ func Test_viewRun(t *testing.T) { Selector: "1234", ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Files: map[string]*shared.GistFile{ "cicada.txt": { Content: "bwhiizzzbwhuiiizzzz", @@ -243,7 +243,7 @@ func Test_viewRun(t *testing.T) { Selector: "1234", ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Files: map[string]*shared.GistFile{ "cicada.txt": { Content: "bwhiizzzbwhuiiizzzz\n", @@ -264,7 +264,7 @@ func Test_viewRun(t *testing.T) { Selector: "1234", ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Description: "some files", Files: map[string]*shared.GistFile{ "cicada.txt": { @@ -287,7 +287,7 @@ func Test_viewRun(t *testing.T) { Raw: true, ListFiles: false, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Description: "some files", Files: map[string]*shared.GistFile{ "cicada.txt": { @@ -310,7 +310,7 @@ func Test_viewRun(t *testing.T) { Raw: false, ListFiles: true, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Description: "some files", Files: map[string]*shared.GistFile{ "cicada.txt": { @@ -329,7 +329,7 @@ func Test_viewRun(t *testing.T) { Raw: false, ListFiles: true, }, - gist: &shared.Gist{ + mockGist: &shared.Gist{ Description: "some files", Files: map[string]*shared.GistFile{ "cicada.txt": { @@ -348,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 { From 5b6fd53a88498caab47c0e24e06c427f53574814 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:36:48 -0700 Subject: [PATCH 0231/2338] Fix: gist edit test name --- pkg/cmd/gist/edit/edit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index 5a3f0a2686b..728f6e89423 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -242,7 +242,7 @@ func Test_editRun(t *testing.T) { }, }, { - name: "multiple files, cancel", + name: "multiple files, cancel, with TTY", isTTY: true, opts: &EditOptions{ Selector: "1234", From facd0caa2971ad3620f451d1968e04aab2cce17c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:44:46 -0700 Subject: [PATCH 0232/2338] Fix: accidental whitespace in gist edit --- pkg/cmd/gist/edit/edit.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index b8511893764..e0d6e3b84a7 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -113,7 +113,6 @@ func editRun(opts *EditOptions) error { } gist, err := shared.PromptGists(opts.Prompter, client, host, cs) - if err != nil { return err } From dcf5cc872412bb8a40554c22b0601394db3d3ff0 Mon Sep 17 00:00:00 2001 From: Rob Morgan Date: Fri, 17 Jan 2025 18:08:59 +0800 Subject: [PATCH 0233/2338] fix: tiny typo fix --- internal/gh/gh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/gh/gh.go b/internal/gh/gh.go index e4431fdab61..8e640c41a88 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -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. From 7942be56aad37d163e970df17d0b5a4cdf2c09c1 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:07:32 -0700 Subject: [PATCH 0234/2338] Add affected version to bug report form --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 00f2a0ba533..ae0d29096a4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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 From 31bff5b5a8020b684566c73e2618fc65b3b8daa4 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 8 Jan 2025 02:30:08 +0000 Subject: [PATCH 0235/2338] Better handling for codespace state polling --- internal/codespaces/api/api.go | 2 + internal/codespaces/codespaces.go | 62 +++++---- internal/codespaces/codespaces_test.go | 175 +++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 23 deletions(-) create mode 100644 internal/codespaces/codespaces_test.go diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 9e79eafe190..0d1eaf5b34f 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -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. diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go index da2eacd4439..38a2e903e49 100644 --- a/internal/codespaces/codespaces.go +++ b/internal/codespaces/codespaces.go @@ -13,6 +13,10 @@ 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.NewConstantBackOff(10 * time.Second) + func connectionReady(codespace *api.Codespace) bool { // If the codespace is not available, it is not ready if codespace.State != api.CodespaceStateAvailable { @@ -67,41 +71,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 - } + 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") + // 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)) - return nil, err + 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 diff --git a/internal/codespaces/codespaces_test.go b/internal/codespaces/codespaces_test.go new file mode 100644 index 00000000000..c9d37aca2db --- /dev/null +++ b/internal/codespaces/codespaces_test.go @@ -0,0 +1,175 @@ +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{} + waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, readyCodespace) +} + +func TestWaitUntilCodespaceConnectionReady_PollsApi(t *testing.T) { + t.Parallel() + + apiClient := &mockApiClient{ + onGetCodespace: func() (*api.Codespace, error) { + return readyCodespace, nil + }, + } + waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, &api.Codespace{State: api.CodespaceStateStarting}) +} + +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 + }, + } + waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) +} + +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 + }, + } + waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) +} + +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 + }, + } + waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) +} + +func TestWaitUntilCodespaceConnectionReady_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 + }, + } + waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) +} + +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() {} From 9b8cd7babd342a0a295d92078ef960cbc3cb0c2a Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:11:17 +0000 Subject: [PATCH 0236/2338] Linter --- internal/codespaces/codespaces_test.go | 51 ++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/internal/codespaces/codespaces_test.go b/internal/codespaces/codespaces_test.go index c9d37aca2db..d931b96ef4b 100644 --- a/internal/codespaces/codespaces_test.go +++ b/internal/codespaces/codespaces_test.go @@ -34,7 +34,13 @@ func TestWaitUntilCodespaceConnectionReady_WhenAlreadyReady(t *testing.T) { t.Parallel() apiClient := &mockApiClient{} - waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, readyCodespace) + 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) { @@ -45,7 +51,14 @@ func TestWaitUntilCodespaceConnectionReady_PollsApi(t *testing.T) { return readyCodespace, nil }, } - waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, &api.Codespace{State: api.CodespaceStateStarting}) + 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) { @@ -62,7 +75,13 @@ func TestWaitUntilCodespaceConnectionReady_StartsCodespace(t *testing.T) { return nil }, } - waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) + 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) { @@ -86,7 +105,13 @@ func TestWaitUntilCodespaceConnectionReady_PollsCodespaceUntilReady(t *testing.T return nil }, } - waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) + 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) { @@ -110,10 +135,16 @@ func TestWaitUntilCodespaceConnectionReady_WaitsForShutdownBeforeStarting(t *tes return nil }, } - waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) + 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_DoesntStartTwice(t *testing.T) { +func TestUntilCodespaceConnectionReady_DoesntStartTwice(t *testing.T) { t.Parallel() codespace := &api.Codespace{State: api.CodespaceStateShutdown} @@ -141,7 +172,13 @@ func TestWaitUntilCodespaceConnectionReady_DoesntStartTwice(t *testing.T) { return nil }, } - waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace) + 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 { From 0c8591064dec9ed95d7fba10a9decb5fff1ee605 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Mon, 13 Jan 2025 22:09:11 +0000 Subject: [PATCH 0237/2338] Change back to exponential backoff --- internal/codespaces/codespaces.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go index 38a2e903e49..92185120a2e 100644 --- a/internal/codespaces/codespaces.go +++ b/internal/codespaces/codespaces.go @@ -15,7 +15,12 @@ import ( // 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.NewConstantBackOff(10 * time.Second) +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 From 4396e40a31de072f6390c6a50cb67ef2e09ba551 Mon Sep 17 00:00:00 2001 From: Mikel Olasagasti Uranga Date: Mon, 20 Jan 2025 16:27:25 +0100 Subject: [PATCH 0238/2338] Fix: Ensure constant format strings in fmt and printf calls Go 1.24 introduces stricter checks for format string validation. This commit fixes instances where non-constant format strings were used in calls to functions like `fmt.Errorf`, `fmt.Printf`, and similar. Signed-off-by: Mikel Olasagasti Uranga --- pkg/cmd/auth/token/token.go | 2 +- pkg/cmd/codespace/create_test.go | 6 +++--- pkg/cmd/pr/merge/merge.go | 7 ++++--- pkg/cmd/repo/view/view.go | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/auth/token/token.go b/pkg/cmd/auth/token/token.go index 9f4dcc1acad..7ac65d37e57 100644 --- a/pkg/cmd/auth/token/token.go +++ b/pkg/cmd/auth/token/token.go @@ -88,7 +88,7 @@ func tokenRun(opts *TokenOptions) error { if opts.Username != "" { errMsg += fmt.Sprintf(" account %s", opts.Username) } - return fmt.Errorf(errMsg) + return fmt.Errorf("%s", errMsg) } if val != "" { diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go index 6466d9be706..85d311a4764 100644 --- a/pkg/cmd/codespace/create_test.go +++ b/pkg/cmd/codespace/create_test.go @@ -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.Logf("%s", err.Error()) } if got := stdout.String(); got != tt.wantStdout { - t.Logf(t.Name()) + t.Logf("%s", t.Name()) t.Errorf(" stdout = %v, want %v", got, tt.wantStdout) } if got := stderr.String(); got != tt.wantStderr { - t.Logf(t.Name()) + t.Logf("%s", t.Name()) t.Errorf(" stderr = %v, want %v", got, tt.wantStderr) } diff --git a/pkg/cmd/pr/merge/merge.go b/pkg/cmd/pr/merge/merge.go index 6696fac85c6..422b5e93e07 100644 --- a/pkg/cmd/pr/merge/merge.go +++ b/pkg/cmd/pr/merge/merge.go @@ -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 diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index d0370203a60..f8cbcb18e63 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -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.Fprintf(stdout, "%s", readme.Content) fmt.Fprintln(stdout) } From 0ead3398a71663edf8e24dae836c957aaf2cc280 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 21 Jan 2025 17:29:39 +0100 Subject: [PATCH 0239/2338] Bump golang ci lint to work with go 1.24 --- .github/workflows/lint.yml | 4 ++-- pkg/cmd/ruleset/shared/shared.go | 2 +- pkg/cmdutil/json_flags.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0a6c0d9db29..f1ae1e522a4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/pkg/cmd/ruleset/shared/shared.go b/pkg/cmd/ruleset/shared/shared.go index 3ec1c83ee35..536e8a5a9b6 100644 --- a/pkg/cmd/ruleset/shared/shared.go +++ b/pkg/cmd/ruleset/shared/shared.go @@ -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 diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index 440e0e0b889..596c2f216a1 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -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) { From 0a3706a4042f5ef319b8a8e619cfafa741b8dc86 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 21 Jan 2025 18:04:47 +0100 Subject: [PATCH 0240/2338] Remove unncessary printf usage --- pkg/cmd/auth/token/token.go | 3 ++- pkg/cmd/codespace/create_test.go | 6 +++--- pkg/cmd/repo/view/view.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/auth/token/token.go b/pkg/cmd/auth/token/token.go index 7ac65d37e57..d9faac7d8c2 100644 --- a/pkg/cmd/auth/token/token.go +++ b/pkg/cmd/auth/token/token.go @@ -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("%s", errMsg) + return errors.New(errMsg) } if val != "" { diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go index 85d311a4764..c5f11bd2f6d 100644 --- a/pkg/cmd/codespace/create_test.go +++ b/pkg/cmd/codespace/create_test.go @@ -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("%s", err.Error()) + t.Log(err.Error()) } if got := stdout.String(); got != tt.wantStdout { - t.Logf("%s", t.Name()) + t.Log(t.Name()) t.Errorf(" stdout = %v, want %v", got, tt.wantStdout) } if got := stderr.String(); got != tt.wantStderr { - t.Logf("%s", t.Name()) + t.Log(t.Name()) t.Errorf(" stderr = %v, want %v", got, tt.wantStderr) } diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index f8cbcb18e63..06f85d04893 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -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, "%s", readme.Content) + fmt.Fprint(stdout, readme.Content) fmt.Fprintln(stdout) } From 9eb8002764575cef00d11900ac982a76d28dfd7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:58:01 +0000 Subject: [PATCH 0241/2338] Bump github.com/google/go-containerregistry from 0.20.2 to 0.20.3 Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.20.2 to 0.20.3. - [Release notes](https://github.com/google/go-containerregistry/releases) - [Changelog](https://github.com/google/go-containerregistry/blob/main/.goreleaser.yml) - [Commits](https://github.com/google/go-containerregistry/compare/v0.20.2...v0.20.3) --- updated-dependencies: - dependency-name: github.com/google/go-containerregistry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 42 +++++++++++------------ go.sum | 106 +++++++++++++++++++++++++++------------------------------ 2 files changed, 71 insertions(+), 77 deletions(-) diff --git a/go.mod b/go.mod index cf08aa2a7a4..e3a72928728 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,7 @@ module github.com/cli/cli/v2 go 1.22.5 - -toolchain go1.22.6 +toolchain go1.23.5 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -18,12 +17,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/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 @@ -44,11 +43,11 @@ require ( 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.36.3 @@ -66,21 +65,21 @@ require ( github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // 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/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 @@ -107,7 +106,7 @@ 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 @@ -121,7 +120,7 @@ require ( 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/vbatts/tar-split v0.11.6 // indirect github.com/yuin/goldmark v1.5.4 // indirect github.com/yuin/goldmark-emoji v1.0.2 // 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 diff --git a/go.sum b/go.sum index 5bb8cb8f7c7..5745095b829 100644 --- a/go.sum +++ b/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,7 +24,6 @@ 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= @@ -108,9 +106,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 +127,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/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.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/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= @@ -162,8 +159,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 +201,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= @@ -288,8 +285,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= @@ -340,8 +337,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N 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 +366,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= @@ -411,7 +408,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 +432,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,9 +447,8 @@ 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/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.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= @@ -467,18 +461,20 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd 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 +485,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 +511,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 +530,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= @@ -557,7 +552,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= From 113fe646f35aa4cb10f0e29446e7b1a4e17ffe99 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 21 Jan 2025 17:13:31 +0100 Subject: [PATCH 0242/2338] Bump go module version to 1.23 --- docs/source.md | 2 +- go.mod | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source.md b/docs/source.md index d2b7c5b3199..29bf51e39f3 100644 --- a/docs/source.md +++ b/docs/source.md @@ -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 diff --git a/go.mod b/go.mod index e3a72928728..ef56b8a73b7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/cli/cli/v2 -go 1.22.5 +go 1.23.0 + toolchain go1.23.5 require ( From bad7098cfe322cd251132403afa23e82332a8e9e Mon Sep 17 00:00:00 2001 From: petercover Date: Thu, 23 Jan 2025 00:47:27 +0800 Subject: [PATCH 0243/2338] chore: fix some comments Signed-off-by: petercover --- README.md | 2 +- pkg/cmd/codespace/ssh_test.go | 2 +- pkg/cmd/factory/default.go | 2 +- pkg/cmd/run/shared/shared.go | 2 +- pkg/iostreams/iostreams.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 60ab7d6d947..c227cffdcf9 100644 --- a/README.md +++ b/README.md @@ -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:** diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index cd04f05fa63..f19ecd09b06 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -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" diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index 0e8bd7856bf..9236514877c 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -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 diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index a583f3a0be0..040888ab5ff 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -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. diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index be057c4941f..2bc712a3cd4 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -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 From d4aebb1cbb3c56b173356844a6bc222c0027297e Mon Sep 17 00:00:00 2001 From: Dennis Ameling Date: Wed, 22 Jan 2025 18:06:40 +0100 Subject: [PATCH 0244/2338] Enable MSI building for Windows arm64 --- .github/workflows/deployment.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index a8356272e95..ebafc2ec832 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -203,10 +203,8 @@ jobs: platform="x64" ;; *_arm64 ) - echo "skipping building MSI for arm64 because WiX 3.11 doesn't support it: https://github.com/wixtoolset/issues/issues/6141" >&2 - continue - #source_dir="$PWD/dist/windows_windows_arm64" - #platform="arm64" + source_dir="$PWD/dist/windows_windows_arm64" + platform="arm64" ;; * ) printf "unsupported architecture: %s\n" "$MSI_NAME" >&2 From 2ec70a5dc39e4bdbe110214d72761062f74c9b5d Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 22 Jan 2025 13:22:45 -0700 Subject: [PATCH 0245/2338] bold bullet point list header text Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/io/handler.go | 2 +- pkg/cmd/attestation/verify/verify.go | 43 +++++++++++++++------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go index 06d747bf40e..579b9a948b0 100644 --- a/pkg/cmd/attestation/io/handler.go +++ b/pkg/cmd/attestation/io/handler.go @@ -82,7 +82,7 @@ func (h *Handler) PrintBulletPoints(rows [][]string) (int, error) { info := "" for _, row := range rows { dots := strings.Repeat(".", maxColLen-len(row[0])) - info += fmt.Sprintf("%s:%s %s\n", row[0], dots, row[1]) + info += fmt.Sprintf(h.ColorScheme.Bold("%s:%s %s\n"), h.ColorScheme.Bold(row[0]), dots, row[1]) } return fmt.Fprintln(h.IO.ErrOut, info) } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 9c285e5c3ee..ea7502f004c 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -262,30 +262,33 @@ func runVerify(opts *Options) error { return nil } - opts.Logger.Printf("%s matched the policy criteria:\n", text.Pluralize(len(verified), "attestation")) + opts.Logger.Printf("The following %s matched the policy criteria\n\n", text.Pluralize(len(verified), "attestation")) // Otherwise print the results to the terminal - buildConfigURI := verified[0].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 := verified[0].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 - } + 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 + } - rows := [][]string{ - {"- Build repo", sourceRepoAndOrg}, - {"- Build workflow", sourceWorkflow}, - {"- Signer repo", signerRepoAndOrg}, - {"- Signer workflow", signerWorkflow}, + 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) } - //nolint:errcheck - opts.Logger.PrintBulletPoints(rows) // All attestations passed verification and policy evaluation return nil From 5f6e720a5030e3a289d3b53fd242a7d0112da9b5 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 22 Jan 2025 13:27:56 -0700 Subject: [PATCH 0246/2338] reorder policy enforcement criteria print out Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/policy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go index 7bae2eff9c4..f2bd126d096 100644 --- a/pkg/cmd/attestation/verification/policy.go +++ b/pkg/cmd/attestation/verification/policy.go @@ -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 { From b19e682455a4c57aff4be5d98a86320eeb4ba0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Kek=C3=A4l=C3=A4inen?= Date: Sat, 28 Dec 2024 13:36:10 -0800 Subject: [PATCH 0247/2338] Update test to be compatible with latest Glamour v0.8.0 Latest Glamour has slightly changed logic in line length / wrapping, resulting test failures due to string mismatch. Update tests and bump dependency to v0.8.0, and others to the bare minimal level as generated by `go mod tidy`. This was detected then building the GitHub cli package `gh` in Debian started to fail with src:golang-github-charmbracelet-glamour 0.8.0-1. Closes: #10179 --- go.mod | 21 ++++++------ go.sum | 56 ++++++++++++++++--------------- pkg/cmd/gist/view/view_test.go | 4 +-- pkg/cmd/pr/create/create_test.go | 4 +-- pkg/cmd/pr/review/review_test.go | 2 +- pkg/cmd/release/view/view_test.go | 8 ++--- pkg/cmd/repo/view/view_test.go | 10 +++--- 7 files changed, 53 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index ef56b8a73b7..2484bbd1561 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( 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 @@ -57,13 +57,13 @@ require ( ) 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.16.3 // indirect @@ -71,7 +71,7 @@ require ( 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/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 @@ -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 @@ -113,13 +113,12 @@ require ( 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 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect @@ -150,8 +149,8 @@ require ( 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.6 // indirect - github.com/yuin/goldmark v1.5.4 // indirect - github.com/yuin/goldmark-emoji v1.0.2 // 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/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect diff --git a/go.sum b/go.sum index 5745095b829..e00b98d5501 100644 --- a/go.sum +++ b/go.sum @@ -28,12 +28,12 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ 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= @@ -70,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= @@ -84,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= @@ -129,8 +133,8 @@ github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1G github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= 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.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +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= @@ -219,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= @@ -315,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= @@ -325,16 +329,14 @@ 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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -449,12 +451,12 @@ github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= 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.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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= diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 706b85f103e..704766e10f8 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -234,7 +234,7 @@ 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", @@ -277,7 +277,7 @@ 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", diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index b8f5daf6638..992439df1e2 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -496,7 +496,7 @@ func Test_createRun(t *testing.T) { `MaintainerCanModify: false`, `Body:`, ``, - ` my body `, + ` my body `, ``, ``, }, @@ -568,7 +568,7 @@ func Test_createRun(t *testing.T) { `MaintainerCanModify: false`, `Body:`, ``, - ` BODY `, + ` BODY `, ``, ``, }, diff --git a/pkg/cmd/pr/review/review_test.go b/pkg/cmd/pr/review/review_test.go index f9e00c3b8ee..611f71894a1 100644 --- a/pkg/cmd/pr/review/review_test.go +++ b/pkg/cmd/pr/review/review_test.go @@ -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()) diff --git a/pkg/cmd/release/view/view_test.go b/pkg/cmd/release/view/view_test.go index 629c0728dc2..6fc747f69c9 100644 --- a/pkg/cmd/release/view/view_test.go +++ b/pkg/cmd/release/view/view_test.go @@ -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 diff --git a/pkg/cmd/repo/view/view_test.go b/pkg/cmd/repo/view/view_test.go index f07f9de187a..d3b6d619b3e 100644 --- a/pkg/cmd/repo/view/view_test.go +++ b/pkg/cmd/repo/view/view_test.go @@ -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 " From 4134f9606e4ec7329cfc700009589c358604c1df Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 23 Jan 2025 07:47:38 -0700 Subject: [PATCH 0248/2338] remove bolding from bullet point output Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/io/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go index 579b9a948b0..06d747bf40e 100644 --- a/pkg/cmd/attestation/io/handler.go +++ b/pkg/cmd/attestation/io/handler.go @@ -82,7 +82,7 @@ func (h *Handler) PrintBulletPoints(rows [][]string) (int, error) { info := "" for _, row := range rows { dots := strings.Repeat(".", maxColLen-len(row[0])) - info += fmt.Sprintf(h.ColorScheme.Bold("%s:%s %s\n"), h.ColorScheme.Bold(row[0]), dots, row[1]) + info += fmt.Sprintf("%s:%s %s\n", row[0], dots, row[1]) } return fmt.Fprintln(h.IO.ErrOut, info) } From fe3d18c96a28e6b2a38fdd16a0e7304ed888e386 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:53:49 +0000 Subject: [PATCH 0249/2338] Bump actions/attest-build-provenance from 2.1.0 to 2.2.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/7668571508540a607bdfd90a87a560489fe372eb...520d128f165991a6c774bcb264f323e3d70747f4) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index a8356272e95..617d9d8249d 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -299,7 +299,7 @@ jobs: rpmsign --addsign dist/*.rpm - name: Attest release artifacts if: inputs.environment == 'production' - uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 + uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0 with: subject-path: "dist/gh_*" - name: Run createrepo From 41729b004def8e1d827f763a0f236afc5fa448b4 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 22 Jan 2025 15:39:43 -0800 Subject: [PATCH 0250/2338] Refactor finder.Find and replace parseCurrentBranch with parsePRRefs I've been struggling horribly to reason through all of this code, and after much mental gymnastics I identified the culprit as the overloaded "branch" string returned by parseCurrentBranch. This value was either the name of the branch that the PR we're looking for is associated with, or that name prepended with the owner's name and a : if we're on a branch, so: PR branch: featureBranch branch == "featureBranch" If on Fork belonging to "ForkOwner" branch == "ForkOwner:featureBranch" Since this extra information was bundled up into this single string, it complicated the responsibilities of parseCurrentBranch's "branch" return value. Thus, I've teased out "branch" into the new PRRefs struct: type PRRefs struct{ BranchName string HeadRepo ghrepo.Interface BaseRepo ghrepo.Interface } This allows the new parsePRRefs function to move all the previous "branch" string's information into structured data, and allows for a new method on PRRefs, GetPRLabel(), to create the string that "branch" previously held to pass into its downstream consumer, namely findForBranch. This also allowed for better test coverage, directly connecting the PRRefs fields to the values contained in the git config. Overall, I am now confident that this is doing what its supposed to do with respect to my understanding of the various central and triangular git workflows we are addressing. --- git/objects.go | 6 +- pkg/cmd/pr/shared/finder.go | 197 ++++++----- pkg/cmd/pr/shared/finder_test.go | 569 +++++++++++++++++++++---------- 3 files changed, 513 insertions(+), 259 deletions(-) diff --git a/git/objects.go b/git/objects.go index ff4068173b9..c6176fb73d3 100644 --- a/git/objects.go +++ b/git/objects.go @@ -64,8 +64,10 @@ type BranchConfig struct { RemoteName string RemoteURL *url.URL // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. - MergeBase string - MergeRef string + MergeBase string + MergeRef string + // These are used to handle triangular workflows. They can be defined by either + // a remote.pushDefault or a branch..pushremote value set on the git config. RemotePushDefault string PushRemoteURL *url.URL PushRemoteName string diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index a707b034e3a..06a66e93d0b 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -41,9 +41,9 @@ type finder struct { branchConfig func(string) (git.BranchConfig, error) progress progressIndicator - repo ghrepo.Interface - prNumber int - branchName string + baseRefRepo ghrepo.Interface + prNumber int + branchName string } func NewFinder(factory *cmdutil.Factory) PRFinder { @@ -89,6 +89,22 @@ type FindOptions struct { States []string } +type PRRefs struct { + BranchName string + HeadRepo ghrepo.Interface + BaseRepo ghrepo.Interface +} + +// GetPRLabel returns the string that the GitHub API uses to identify the PR. This is +// either just the branch name or, if the PR is originating from a fork, the fork owner +// and the branch name, like :. +func (s *PRRefs) GetPRLabel() string { + if s.HeadRepo == s.BaseRepo { + return s.BranchName + } + return fmt.Sprintf("%s:%s", s.HeadRepo.RepoOwner(), s.BranchName) +} + func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) { if len(opts.Fields) == 0 { return nil, nil, errors.New("Find error: no fields specified") @@ -96,26 +112,18 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err if repo, prNumber, err := f.parseURL(opts.Selector); err == nil { f.prNumber = prNumber - f.repo = repo + f.baseRefRepo = repo } - if f.repo == nil { + if f.baseRefRepo == nil { repo, err := f.baseRepoFn() if err != nil { return nil, nil, err } - f.repo = repo + f.baseRefRepo = repo } - if opts.Selector == "" { - if branch, prNumber, err := f.parseCurrentBranch(); err != nil { - return nil, nil, err - } else if prNumber > 0 { - f.prNumber = prNumber - } else { - f.branchName = branch - } - } else if f.prNumber == 0 { + if f.prNumber == 0 && opts.Selector != "" { // If opts.Selector is a valid number then assume it is the // PR number unless opts.BaseBranch is specified. This is a // special case for PR create command which will always want @@ -127,8 +135,28 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err } else { f.branchName = opts.Selector } + } else { + currentBranchName, err := f.branchFn() + if err != nil { + return nil, nil, err + } + f.branchName = currentBranchName } + // Get the branch config for the current branchName + branchConfig, err := f.branchConfig(f.branchName) + if err != nil { + return nil, nil, err + } + + // Determine if the branch is configured to merge to a special PR ref + prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`) + if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil { + prNumber, _ := strconv.Atoi(m[1]) + f.prNumber = prNumber + } + + // Set up HTTP client httpClient, err := f.httpClient() if err != nil { return nil, nil, err @@ -147,7 +175,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err if fields.Contains("isInMergeQueue") || fields.Contains("isMergeQueueEnabled") { cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) - detector := fd.NewDetector(cachedClient, f.repo.RepoHost()) + detector := fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost()) prFeatures, err := detector.PullRequestFeatures() if err != nil { return nil, nil, err @@ -168,36 +196,54 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err if f.prNumber > 0 { if numberFieldOnly { // avoid hitting the API if we already have all the information - return &api.PullRequest{Number: f.prNumber}, f.repo, nil + return &api.PullRequest{Number: f.prNumber}, f.baseRefRepo, nil + } + pr, err = findByNumber(httpClient, f.baseRefRepo, f.prNumber, fields.ToSlice()) + if err != nil { + return pr, f.baseRefRepo, err } - pr, err = findByNumber(httpClient, f.repo, f.prNumber, fields.ToSlice()) } else { - pr, err = findForBranch(httpClient, f.repo, opts.BaseBranch, f.branchName, opts.States, fields.ToSlice()) - } - if err != nil { - return pr, f.repo, err + rems, err := f.remotesFn() + if err != nil { + return nil, nil, err + } + + pushDefault, err := f.pushDefault() + if err != nil { + return nil, nil, err + } + + prRefs, err := parsePRRefs(f.branchName, branchConfig, pushDefault, f.baseRefRepo, rems) + if err != nil { + return nil, nil, err + } + + pr, err = findForBranch(httpClient, f.baseRefRepo, opts.BaseBranch, prRefs.GetPRLabel(), opts.States, fields.ToSlice()) + if err != nil { + return pr, f.baseRefRepo, err + } } g, _ := errgroup.WithContext(context.Background()) if fields.Contains("reviews") { g.Go(func() error { - return preloadPrReviews(httpClient, f.repo, pr) + return preloadPrReviews(httpClient, f.baseRefRepo, pr) }) } if fields.Contains("comments") { g.Go(func() error { - return preloadPrComments(httpClient, f.repo, pr) + return preloadPrComments(httpClient, f.baseRefRepo, pr) }) } if fields.Contains("statusCheckRollup") { g.Go(func() error { - return preloadPrChecks(httpClient, f.repo, pr) + return preloadPrChecks(httpClient, f.baseRefRepo, pr) }) } if getProjectItems { g.Go(func() error { apiClient := api.NewClientFromHTTP(httpClient) - err := api.ProjectsV2ItemsForPullRequest(apiClient, f.repo, pr) + err := api.ProjectsV2ItemsForPullRequest(apiClient, f.baseRefRepo, pr) if err != nil && !api.ProjectsV2IgnorableError(err) { return err } @@ -205,7 +251,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err }) } - return pr, f.repo, g.Wait() + return pr, f.baseRefRepo, g.Wait() } var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`) @@ -234,61 +280,53 @@ func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) { return repo, prNumber, nil } -var prHeadRE = regexp.MustCompile(`^refs/pull/(\d+)/head$`) - -func (f *finder) parseCurrentBranch() (string, int, error) { - prHeadRef, err := f.branchFn() - if err != nil { - return "", 0, err - } - - branchConfig, err := f.branchConfig(prHeadRef) - if err != nil { - return "", 0, err +func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, pushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { + prRefs := PRRefs{ + BaseRepo: baseRefRepo, } - // the branch is configured to merge a special PR head ref - if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil { - prNumber, _ := strconv.Atoi(m[1]) - return "", prNumber, nil + // If @{push} resolves, then we have all the information we need to determine the head repo + // and branch name. It is of the form /. + if branchConfig.Push != "" { + for _, r := range rems { + // Find the remote who's name matches the push prefix + if strings.HasPrefix(branchConfig.Push, r.Name+"/") { + prRefs.BranchName = strings.TrimPrefix(branchConfig.Push, r.Name+"/") + prRefs.HeadRepo = r.Repo + return prRefs, nil + } + } } - var gitRemoteRepo ghrepo.Interface - if branchConfig.PushRemoteURL != nil { - // the branch merges from a remote specified by URL - if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil { - gitRemoteRepo = r + // To get the HeadRepo, we look to the git config. The PushRemote{Name | URL} comes from + // one of the following, in order of precedence: + // 1. branch..pushRemote + // 2. remote.pushDefault + // 3. branch..remote + if branchConfig.PushRemoteName != "" { + if r, err := rems.FindByName(branchConfig.PushRemoteName); err == nil { + prRefs.HeadRepo = r.Repo } - } else if branchConfig.PushRemoteName != "" { - rem, _ := f.remotesFn() - if r, err := rem.FindByName(branchConfig.PushRemoteName); err == nil { - gitRemoteRepo = r + } else if branchConfig.PushRemoteURL != nil { + if r, err := ghrepo.FromURL(branchConfig.PushRemoteURL); err == nil { + prRefs.HeadRepo = r } } - if gitRemoteRepo != nil { - if branchConfig.Push != "" { - prHeadRef = strings.TrimPrefix(branchConfig.Push, branchConfig.PushRemoteName+"/") - } else if pushDefault, _ := f.pushDefault(); (pushDefault == "upstream" || pushDefault == "tracking") && - strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { - prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") - } - // prepend `OWNER:` if this branch is pushed to a fork - // This is determined by: - // - The repo having a different owner - // - The repo having the same owner but a different name (private org fork) - // I suspect that the implementation of the second case may be broken in the face - // of a repo rename, where the remote hasn't been updated locally. This is a - // frequent issue in commands that use SmartBaseRepoFunc. It's not any worse than not - // supporting this case at all though. - sameOwner := strings.EqualFold(gitRemoteRepo.RepoOwner(), f.repo.RepoOwner()) - sameOwnerDifferentRepoName := sameOwner && !strings.EqualFold(gitRemoteRepo.RepoName(), f.repo.RepoName()) - if !sameOwner || sameOwnerDifferentRepoName { - prHeadRef = fmt.Sprintf("%s:%s", gitRemoteRepo.RepoOwner(), prHeadRef) - } + // We assume the PR's branch name is the same as whatever f.BranchFn() returned earlier. + // unless the user has specified push.default = upstream or tracking, then we use the + // branch name from the merge ref. + prRefs.BranchName = currentBranchName + if pushDefault == "upstream" || pushDefault == "tracking" { + prRefs.BranchName = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") } - return prHeadRef, 0, nil + // The PR merges from a branch in the same repo as the base branch (usually the default branch) + if prRefs.HeadRepo == nil { + prRefs.HeadRepo = baseRefRepo + } + + return prRefs, nil } func findByNumber(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.PullRequest, error) { @@ -321,7 +359,7 @@ func findByNumber(httpClient *http.Client, repo ghrepo.Interface, number int, fi return &resp.Repository.PullRequest, nil } -func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters, fields []string) (*api.PullRequest, error) { +func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, headBranchWithOwnerIfFork string, stateFilters, fields []string) (*api.PullRequest, error) { type response struct { Repository struct { PullRequests struct { @@ -348,9 +386,9 @@ func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, h } }`, api.PullRequestGraphQL(fieldSet.ToSlice())) - branchWithoutOwner := headBranch - if idx := strings.Index(headBranch, ":"); idx >= 0 { - branchWithoutOwner = headBranch[idx+1:] + branchWithoutOwner := headBranchWithOwnerIfFork + if idx := strings.Index(headBranchWithOwnerIfFork, ":"); idx >= 0 { + branchWithoutOwner = headBranchWithOwnerIfFork[idx+1:] } variables := map[string]interface{}{ @@ -373,18 +411,17 @@ func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, h }) for _, pr := range prs { - headBranchMatches := pr.HeadLabel() == headBranch + headBranchMatches := pr.HeadLabel() == headBranchWithOwnerIfFork baseBranchEmptyOrMatches := baseBranch == "" || pr.BaseRefName == baseBranch // When the head is the default branch, it doesn't really make sense to show merged or closed PRs. // https://github.com/cli/cli/issues/4263 - isNotClosedOrMergedWhenHeadIsDefault := pr.State == "OPEN" || resp.Repository.DefaultBranchRef.Name != headBranch - + isNotClosedOrMergedWhenHeadIsDefault := pr.State == "OPEN" || resp.Repository.DefaultBranchRef.Name != headBranchWithOwnerIfFork if headBranchMatches && baseBranchEmptyOrMatches && isNotClosedOrMergedWhenHeadIsDefault { return &pr, nil } } - return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)} + return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranchWithOwnerIfFork)} } func preloadPrReviews(httpClient *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error { diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 340f6981094..7a01400886b 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -18,20 +18,45 @@ type args struct { baseRepoFn func() (ghrepo.Interface, error) branchFn func() (string, error) branchConfig func(string) (git.BranchConfig, error) - remotesFn func() (context.Remotes, error) + pushDefault func() (string, 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, error) - pushDefault func() (string, error) - remotesFn func() (context.Remotes, error) - selector string - fields []string - baseBranch string + // TODO: Abstract these out meaningfully for reuse in parsePRRefs tests + originOwnerUrl, err := url.Parse("https://github.com/ORIGINOWNER/REPO.git") + if err != nil { + t.Fatal(err) } + remoteOrigin := context.Remote{ + Remote: &git.Remote{ + Name: "origin", + FetchURL: originOwnerUrl, + }, + Repo: ghrepo.New("ORIGINOWNER", "REPO"), + } + remoteOther := context.Remote{ + Remote: &git.Remote{ + Name: "other", + FetchURL: originOwnerUrl, + }, + Repo: ghrepo.New("ORIGINOWNER", "OTHER-REPO"), + } + + upstreamOwnerUrl, err := url.Parse("https://github.com/UPSTREAMOWNER/REPO.git") + if err != nil { + t.Fatal(err) + } + remoteUpstream := context.Remote{ + Remote: &git.Remote{ + Name: "upstream", + FetchURL: upstreamOwnerUrl, + }, + Repo: ghrepo.New("UPSTREAMOWNER", "REPO"), + } + tests := []struct { name string args args @@ -43,11 +68,13 @@ func TestFind(t *testing.T) { { name: "number argument", args: args{ - selector: "13", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") + selector: "13", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + branchFn: func() (string, error) { + return "blueberries", nil }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -57,7 +84,7 @@ func TestFind(t *testing.T) { }}}`)) }, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { name: "number argument with base branch", @@ -65,9 +92,14 @@ func TestFind(t *testing.T) { selector: "13", baseBranch: "main", fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + branchFn: func() (string, error) { + return "blueberries", nil }, + branchConfig: stubBranchConfig(git.BranchConfig{ + PushRemoteName: remoteOrigin.Remote.Name, + }, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -80,22 +112,25 @@ func TestFind(t *testing.T) { "baseRefName": "main", "headRefName": "13", "isCrossRepository": false, - "headRepositoryOwner": {"login":"OWNER"} + "headRepositoryOwner": {"login":"ORIGINOWNER"} } ]} }}}`)) }, wantPR: 123, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { name: "baseRepo is error", args: args{ - selector: "13", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return nil, errors.New("baseRepoErr") + selector: "13", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(nil, errors.New("baseRepoErr")), + branchFn: func() (string, error) { + return "blueberries", nil }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, wantErr: true, }, @@ -110,24 +145,30 @@ func TestFind(t *testing.T) { { name: "number only", args: args{ - selector: "13", - fields: []string{"number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") + selector: "13", + fields: []string{"number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + branchFn: func() (string, error) { + return "blueberries", nil }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: nil, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { name: "number with hash argument", args: args{ - selector: "#13", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") + selector: "#13", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + branchFn: func() (string, error) { + return "blueberries", nil }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -137,14 +178,19 @@ func TestFind(t *testing.T) { }}}`)) }, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { - name: "URL argument", + name: "PR URL argument", args: args{ selector: "https://example.org/OWNER/REPO/pull/13/files", fields: []string{"id", "number"}, baseRepoFn: nil, + branchFn: func() (string, error) { + return "blueberries", nil + }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -157,13 +203,16 @@ func TestFind(t *testing.T) { wantRepo: "https://example.org/OWNER/REPO", }, { - name: "branch argument", + name: "when provided branch argument with an open and closed PR for that branch name, it returns the open PR", args: args{ - selector: "blueberries", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") + selector: "blueberries", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + branchFn: func() (string, error) { + return "blueberries", nil }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -176,7 +225,7 @@ func TestFind(t *testing.T) { "baseRefName": "main", "headRefName": "blueberries", "isCrossRepository": false, - "headRepositoryOwner": {"login":"OWNER"} + "headRepositoryOwner": {"login":"ORIGINOWNER"} }, { "number": 13, @@ -184,13 +233,13 @@ func TestFind(t *testing.T) { "baseRefName": "main", "headRefName": "blueberries", "isCrossRepository": false, - "headRepositoryOwner": {"login":"OWNER"} + "headRepositoryOwner": {"login":"ORIGINOWNER"} } ]} }}}`)) }, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { name: "branch argument with base branch", @@ -201,6 +250,11 @@ func TestFind(t *testing.T) { baseRepoFn: func() (ghrepo.Interface, error) { return ghrepo.FromFullName("OWNER/REPO") }, + branchFn: func() (string, error) { + return "blueberries", nil + }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -240,17 +294,8 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{ - MergeRef: "refs/heads/blueberries", - RemoteName: "origin", - Push: "origin/blueberries", - }, nil), - remotesFn: func() (context.Remotes, error) { - return context.Remotes{{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("OWNER", "REPO"), - }}, nil - }, + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -283,6 +328,7 @@ func TestFind(t *testing.T) { return "blueberries", nil }, branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -320,70 +366,19 @@ func TestFind(t *testing.T) { wantErr: true, }, { - name: "current branch with upstream configuration", + name: "when the current branch is configured to push to and pull from 'upstream' and push.default = upstream but the repo push/pulls from 'origin', it finds the PR associated with the upstream repo and returns origin as the base repo", args: args{ - selector: "", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") - }, + selector: "", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), branchFn: func() (string, error) { return "blueberries", nil }, - pushDefault: func() (string, error) { return "upstream", nil }, branchConfig: stubBranchConfig(git.BranchConfig{ MergeRef: "refs/heads/blue-upstream-berries", - RemoteName: "origin", - PushRemoteName: "origin", - Push: "origin/blue-upstream-berries", + PushRemoteName: "upstream", }, nil), - remotesFn: func() (context.Remotes, error) { - return context.Remotes{{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("UPSTREAMOWNER", "REPO"), - }}, nil - }, - }, - httpStub: func(r *httpmock.Registry) { - r.Register( - httpmock.GraphQL(`query PullRequestForBranch\b`), - httpmock.StringResponse(`{"data":{"repository":{ - "pullRequests":{"nodes":[ - { - "number": 13, - "state": "OPEN", - "baseRefName": "main", - "headRefName": "blue-upstream-berries", - "isCrossRepository": true, - "headRepositoryOwner": {"login":"UPSTREAMOWNER"} - } - ]} - }}}`)) - }, - wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", - }, - { - name: "current branch with upstream RemoteURL configuration", - args: args{ - selector: "", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") - }, - branchFn: func() (string, error) { - return "blueberries", nil - }, - branchConfig: func(branch string) (git.BranchConfig, error) { - u, _ := url.Parse("https://github.com/UPSTREAMOWNER/REPO") - return stubBranchConfig(git.BranchConfig{ - MergeRef: "refs/heads/blue-upstream-berries", - RemoteURL: u, - PushRemoteURL: u, - }, nil)(branch) - }, - pushDefault: func() (string, error) { return "upstream", nil }, - remotesFn: nil, + pushDefault: stubPushDefault("upstream", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -402,32 +397,22 @@ func TestFind(t *testing.T) { }}}`)) }, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { - name: "current branch with tracking (deprecated synonym of upstream) configuration", + name: "the current branch is configured to push to and pull from a URL (upstream, in this example) that is different from what the repo is configured to push to and pull from (origin, in this example) and push.default = upstream, it finds the PR associated with the upstream repo and returns origin as the base repo", args: args{ - selector: "", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") - }, + selector: "", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), branchFn: func() (string, error) { return "blueberries", nil }, branchConfig: stubBranchConfig(git.BranchConfig{ - MergeRef: "refs/heads/blue-upstream-berries", - RemoteName: "origin", - PushRemoteName: "origin", - Push: "origin/blue-upstream-berries", + MergeRef: "refs/heads/blue-upstream-berries", + PushRemoteURL: remoteUpstream.Remote.FetchURL, }, nil), - pushDefault: func() (string, error) { return "tracking", nil }, - remotesFn: func() (context.Remotes, error) { - return context.Remotes{{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("UPSTREAMOWNER", "REPO"), - }}, nil - }, + pushDefault: stubPushDefault("upstream", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -446,34 +431,21 @@ func TestFind(t *testing.T) { }}}`)) }, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { name: "current branch with upstream and fork in same org", args: args{ - selector: "", - fields: []string{"id", "number"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") - }, + selector: "", + fields: []string{"id", "number"}, + baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), branchFn: func() (string, error) { return "blueberries", nil }, branchConfig: stubBranchConfig(git.BranchConfig{ - RemoteName: "origin", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - Push: "origin/blueberries", + Push: "other/blueberries", }, nil), - remotesFn: func() (context.Remotes, error) { - return context.Remotes{{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("OWNER", "REPO-FORK"), - }, { - Remote: &git.Remote{Name: "upstream"}, - Repo: ghrepo.New("OWNER", "REPO"), - }}, nil - }, + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -486,13 +458,13 @@ func TestFind(t *testing.T) { "baseRefName": "main", "headRefName": "blueberries", "isCrossRepository": true, - "headRepositoryOwner": {"login":"OWNER"} + "headRepositoryOwner": {"login":"ORIGINOWNER"} } ]} }}}`)) }, wantPR: 13, - wantRepo: "https://github.com/OWNER/REPO", + wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { name: "current branch made by pr checkout", @@ -533,6 +505,7 @@ func TestFind(t *testing.T) { branchConfig: stubBranchConfig(git.BranchConfig{ MergeRef: "refs/pull/13/head", }, nil), + pushDefault: stubPushDefault("simple", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -597,7 +570,11 @@ func TestFind(t *testing.T) { branchFn: tt.args.branchFn, branchConfig: tt.args.branchConfig, pushDefault: tt.args.pushDefault, - remotesFn: tt.args.remotesFn, + remotesFn: stubRemotes(context.Remotes{ + &remoteOrigin, + &remoteOther, + &remoteUpstream, + }, nil), } pr, repo, err := f.Find(FindOptions{ @@ -630,42 +607,262 @@ func TestFind(t *testing.T) { } } -func Test_parseCurrentBranch(t *testing.T) { +func Test_parsePRRefs(t *testing.T) { + originOwnerUrl, err := url.Parse("https://github.com/ORIGINOWNER/REPO.git") + if err != nil { + t.Fatal(err) + } + remoteOrigin := context.Remote{ + Remote: &git.Remote{ + Name: "origin", + FetchURL: originOwnerUrl, + }, + Repo: ghrepo.New("ORIGINOWNER", "REPO"), + } + remoteOther := context.Remote{ + Remote: &git.Remote{ + Name: "other", + FetchURL: originOwnerUrl, + }, + Repo: ghrepo.New("ORIGINOWNER", "REPO"), + } + + upstreamOwnerUrl, err := url.Parse("https://github.com/UPSTREAMOWNER/REPO.git") + if err != nil { + t.Fatal(err) + } + remoteUpstream := context.Remote{ + Remote: &git.Remote{ + Name: "upstream", + FetchURL: upstreamOwnerUrl, + }, + Repo: ghrepo.New("UPSTREAMOWNER", "REPO"), + } + tests := []struct { - name string - args args - wantSelector string - wantPR int - wantError error + name string + branchConfig git.BranchConfig + pushDefault string + currentBranchName string + baseRefRepo ghrepo.Interface + rems context.Remotes + wantPRRefs PRRefs + wantErr error }{ { - name: "failed branch config", - args: args{ - branchConfig: stubBranchConfig(git.BranchConfig{}, errors.New("branchConfigErr")), - branchFn: func() (string, error) { - return "blueberries", nil - }, + name: "When the branch is called 'blueberries' with an empty branch config, it returns the correct PRRefs", + branchConfig: git.BranchConfig{}, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the branch is called 'otherBranch' with an empty branch config, it returns the correct PRRefs", + branchConfig: git.BranchConfig{}, + currentBranchName: "otherBranch", + baseRefRepo: remoteOrigin.Repo, + wantPRRefs: PRRefs{ + BranchName: "otherBranch", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the branch name doesn't match the branch name in BranchConfig.Push, it returns the BranchConfig.Push branch name", + branchConfig: git.BranchConfig{ + Push: "origin/pushBranch", + }, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + }, + wantPRRefs: PRRefs{ + BranchName: "pushBranch", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteOrigin.Repo, }, - wantSelector: "", - wantPR: 0, - wantError: errors.New("branchConfigErr"), + wantErr: nil, + }, + { + name: "When the branch name doesn't match a different branch name in BranchConfig.Push, it returns the BranchConfig.Push branch name", + branchConfig: git.BranchConfig{ + Push: "origin/differentPushBranch", + }, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + }, + wantPRRefs: PRRefs{ + BranchName: "differentPushBranch", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the branch name doesn't match a different branch name in BranchConfig.Push and the remote isn't 'origin', it returns the BranchConfig.Push branch name", + branchConfig: git.BranchConfig{ + Push: "other/pushBranch", + }, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOther, + }, + wantPRRefs: PRRefs{ + BranchName: "pushBranch", + HeadRepo: remoteOther.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the push remote is the same as the baseRepo, it returns the baseRepo as the PRRefs HeadRepo", + branchConfig: git.BranchConfig{ + PushRemoteName: remoteOrigin.Remote.Name, + }, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the push remote is different from the baseRepo, it returns the push remote repo as the PRRefs HeadRepo", + branchConfig: git.BranchConfig{ + PushRemoteName: remoteOrigin.Remote.Name, + }, + currentBranchName: "blueberries", + baseRefRepo: remoteUpstream.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteUpstream.Repo, + }, + wantErr: nil, + }, + { + name: "When the push remote defined by a URL and the baseRepo is different from the push remote, it returns the push remote repo as the PRRefs HeadRepo", + branchConfig: git.BranchConfig{ + PushRemoteURL: remoteOrigin.Remote.FetchURL, + }, + currentBranchName: "blueberries", + baseRefRepo: remoteUpstream.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteOrigin.Repo, + BaseRepo: remoteUpstream.Repo, + }, + wantErr: nil, + }, + { + name: "When the push remote and merge ref are configured to a different repo and push.default = upstream, it should return the branch name from the other repo", + branchConfig: git.BranchConfig{ + PushRemoteName: remoteUpstream.Remote.Name, + MergeRef: "refs/heads/blue-upstream-berries", + }, + pushDefault: "upstream", + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blue-upstream-berries", + HeadRepo: remoteUpstream.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the push remote and merge ref are configured to a different repo and push.default = tracking, it should return the branch name from the other repo", + branchConfig: git.BranchConfig{ + PushRemoteName: remoteUpstream.Remote.Name, + MergeRef: "refs/heads/blue-upstream-berries", + }, + pushDefault: "tracking", + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blue-upstream-berries", + HeadRepo: remoteUpstream.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, }, } 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, + prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.pushDefault, tt.baseRefRepo, tt.rems) + if tt.wantErr != nil { + require.Error(t, err) + assert.Equal(t, tt.wantErr, err) + } else { + require.NoError(t, err) } - selector, pr, err := f.parseCurrentBranch() - assert.Equal(t, tt.wantSelector, selector) - assert.Equal(t, tt.wantPR, pr) - assert.Equal(t, tt.wantError, err) + assert.Equal(t, tt.wantPRRefs, prRefs) + }) + } +} + +func TestPRRefs_GetPRLabel(t *testing.T) { + originRepo := ghrepo.New("ORIGINOWNER", "REPO") + upstreamRepo := ghrepo.New("UPSTREAMOWNER", "REPO") + tests := []struct { + name string + prRefs PRRefs + want string + }{ + { + name: "When the HeadRepo and BaseRepo match, it returns the branch name", + prRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: originRepo, + BaseRepo: originRepo, + }, + want: "blueberries", + }, + { + name: "When the HeadRepo and BaseRepo do not match, it returns the prepended HeadRepo owner to the branch name", + prRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: originRepo, + BaseRepo: upstreamRepo, + }, + want: "ORIGINOWNER:blueberries", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.prRefs.GetPRLabel()) }) } } @@ -675,3 +872,21 @@ func stubBranchConfig(branchConfig git.BranchConfig, err error) func(string) (gi return branchConfig, err } } + +func stubRemotes(remotes context.Remotes, err error) func() (context.Remotes, error) { + return func() (context.Remotes, error) { + return remotes, err + } +} + +func stubBaseRepoFn(baseRepo ghrepo.Interface, err error) func() (ghrepo.Interface, error) { + return func() (ghrepo.Interface, error) { + return baseRepo, err + } +} + +func stubPushDefault(pushDefault string, err error) func() (string, error) { + return func() (string, error) { + return pushDefault, err + } +} From 9f801771a8f198d6219f15e964c1c3b9f205af12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:49:07 +0000 Subject: [PATCH 0251/2338] Bump google.golang.org/protobuf from 1.36.3 to 1.36.4 Bumps google.golang.org/protobuf from 1.36.3 to 1.36.4. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2484bbd1561..ca554958724 100644 --- a/go.mod +++ b/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.64.1 - google.golang.org/protobuf v1.36.3 + google.golang.org/protobuf v1.36.4 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index e00b98d5501..5a7147b193e 100644 --- a/go.sum +++ b/go.sum @@ -545,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.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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= From a72bef9b42e29667d00f0f2032ee9f4336709d9b Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 24 Jan 2025 17:07:24 +0100 Subject: [PATCH 0252/2338] Error if push revision doesn't match a remote --- pkg/cmd/pr/shared/finder.go | 8 +++++++- pkg/cmd/pr/shared/finder_test.go | 35 +++++++++++++++----------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 06a66e93d0b..4aa58af96d8 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -99,7 +99,7 @@ type PRRefs struct { // either just the branch name or, if the PR is originating from a fork, the fork owner // and the branch name, like :. func (s *PRRefs) GetPRLabel() string { - if s.HeadRepo == s.BaseRepo { + if ghrepo.IsSame(s.HeadRepo, s.BaseRepo) { return s.BranchName } return fmt.Sprintf("%s:%s", s.HeadRepo.RepoOwner(), s.BranchName) @@ -296,6 +296,12 @@ func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, pushDe return prRefs, nil } } + + remoteNames := make([]string, len(rems)) + for i, r := range rems { + remoteNames[i] = r.Name + } + return PRRefs{}, fmt.Errorf("no remote for %q found in %q", branchConfig.Push, strings.Join(remoteNames, ", ")) } // To get the HeadRepo, we look to the git config. The PushRemote{Name | URL} comes from diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 7a01400886b..fe01e6b6ec3 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -2,6 +2,7 @@ package shared import ( "errors" + "fmt" "net/http" "net/url" "testing" @@ -70,7 +71,7 @@ func TestFind(t *testing.T) { args: args{ selector: "13", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -92,7 +93,7 @@ func TestFind(t *testing.T) { selector: "13", baseBranch: "main", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -147,7 +148,7 @@ func TestFind(t *testing.T) { args: args{ selector: "13", fields: []string{"number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -163,7 +164,7 @@ func TestFind(t *testing.T) { args: args{ selector: "#13", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -207,7 +208,7 @@ func TestFind(t *testing.T) { args: args{ selector: "blueberries", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -370,7 +371,7 @@ func TestFind(t *testing.T) { args: args{ selector: "", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -404,7 +405,7 @@ func TestFind(t *testing.T) { args: args{ selector: "", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -438,7 +439,7 @@ func TestFind(t *testing.T) { args: args{ selector: "", fields: []string{"id", "number"}, - baseRepoFn: stubBaseRepoFn(remoteOrigin.Repo, nil), + baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil), branchFn: func() (string, error) { return "blueberries", nil }, @@ -691,21 +692,18 @@ func Test_parsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the branch name doesn't match a different branch name in BranchConfig.Push, it returns the BranchConfig.Push branch name", + name: "When the push revision doesn't match a remote, it returns an error", branchConfig: git.BranchConfig{ Push: "origin/differentPushBranch", }, currentBranchName: "blueberries", baseRefRepo: remoteOrigin.Repo, rems: context.Remotes{ - &remoteOrigin, - }, - wantPRRefs: PRRefs{ - BranchName: "differentPushBranch", - HeadRepo: remoteOrigin.Repo, - BaseRepo: remoteOrigin.Repo, + &remoteUpstream, + &remoteOther, }, - wantErr: nil, + wantPRRefs: PRRefs{}, + wantErr: fmt.Errorf("no remote for %q found in %q", "origin/differentPushBranch", "upstream, other"), }, { name: "When the branch name doesn't match a different branch name in BranchConfig.Push and the remote isn't 'origin', it returns the BranchConfig.Push branch name", @@ -823,12 +821,11 @@ func Test_parsePRRefs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.pushDefault, tt.baseRefRepo, tt.rems) if tt.wantErr != nil { - require.Error(t, err) - assert.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantErr, err) } else { require.NoError(t, err) } - assert.Equal(t, tt.wantPRRefs, prRefs) + require.Equal(t, tt.wantPRRefs, prRefs) }) } } From 6355ed7c08e1a6a76cb10b5b461750cdee034dcb Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 24 Jan 2025 17:25:38 +0100 Subject: [PATCH 0253/2338] WIP: push default defaults to simple --- pkg/cmd/pr/shared/finder.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 4aa58af96d8..57db40a851c 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -59,7 +59,17 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { remotesFn: factory.Remotes, httpClient: factory.HttpClient, pushDefault: func() (string, error) { - return factory.GitClient.Config(context.Background(), "push.default") + pushDefault, err := factory.GitClient.Config(context.Background(), "push.default") + if err == nil { + return pushDefault, nil + } + + var gitErr *git.GitError + if ok := errors.As(err, &gitErr); ok && gitErr.ExitCode == 1 { + return "simple", nil + } + + return "", err }, progress: factory.IOStreams, branchConfig: func(s string) (git.BranchConfig, error) { From 2f81a33e95e3a0e1a44b017ecf6446cfcc5701b6 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 09:28:55 -0700 Subject: [PATCH 0254/2338] add new signing options Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/options.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 4296cb8ec08..3ac642904b6 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -31,8 +31,12 @@ type Options struct { Repo string SAN string SANRegex string + SignerDigest string + SignerRef string SignerRepo string SignerWorkflow string + SourceRef string + SourceDigest string APIClient api.Client Logger *io.Handler OCIClient oci.Client From 5df2b47d1ff607196f8951e04eaa61cbd6d12a17 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 09:30:46 -0700 Subject: [PATCH 0255/2338] update tests Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index f89dcddf2ff..f023dd03ad9 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -283,7 +283,7 @@ func TestGetBundle_Retry(t *testing.T) { logger: io.NewTestHandler(), } - b, err := c.getBundle("whatever") + b, err := c.getBundle("mybundleurl") require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2) @@ -296,7 +296,7 @@ func TestGetBundle_Fail(t *testing.T) { logger: io.NewTestHandler(), } - b, err := c.getBundle("whatever") + b, err := c.getBundle("mybundleurl") require.Error(t, err) require.Nil(t, b) mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) From 5a8dd35ba7e99600b43ffa37fd8cace328ce9ab2 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 24 Jan 2025 09:40:02 -0800 Subject: [PATCH 0256/2338] Add PushDefault method to git client --- git/client.go | 15 +++++++++ git/client_test.go | 62 +++++++++++++++++++++++++++++++++++++ pkg/cmd/pr/shared/finder.go | 12 +------ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/git/client.go b/git/client.go index 9ef6c152ebb..c1951f07b76 100644 --- a/git/client.go +++ b/git/client.go @@ -482,6 +482,21 @@ func (c *Client) SetBranchConfig(ctx context.Context, branch, name, value string return err } +// PushDefault returns the value of push.default in the config. If the value +// is not set, it returns "simple" (the default value). +func (c *Client) PushDefault(ctx context.Context) (string, error) { + pushDefault, err := c.Config(ctx, "push.default") + if err == nil { + return pushDefault, nil + } + + var gitError *GitError + if ok := errors.As(err, &gitError); ok && gitError.ExitCode == 1 { + return "simple", nil + } + return "", err +} + func (c *Client) DeleteLocalTag(ctx context.Context, tag string) error { args := []string{"tag", "-d", tag} cmd, err := c.Command(ctx, args...) diff --git a/git/client_test.go b/git/client_test.go index ba13c889ac3..4e35879d94c 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1191,6 +1191,68 @@ func Test_parseRemoteURLOrName(t *testing.T) { } } +func TestClientPushDefault(t *testing.T) { + tests := []struct { + name string + commandResult commandResult + wantPushDefault string + wantError *GitError + }{ + { + name: "push default is not set", + commandResult: commandResult{ + ExitStatus: 1, + Stderr: "error: key does not contain a section: remote.pushDefault", + }, + wantPushDefault: "simple", + wantError: nil, + }, + { + name: "push default is set to current", + commandResult: commandResult{ + ExitStatus: 0, + Stdout: "current", + }, + wantPushDefault: "current", + wantError: nil, + }, + { + name: "push default errors", + commandResult: commandResult{ + ExitStatus: 128, + Stderr: "fatal: git error", + }, + wantPushDefault: "", + wantError: &GitError{ + ExitCode: 128, + Stderr: "fatal: git error", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdCtx := createMockedCommandContext(t, mockedCommands{ + `path/to/git config push.default`: tt.commandResult, + }, + ) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + pushDefault, err := client.PushDefault(context.Background()) + if tt.wantError != nil { + var gitError *GitError + require.ErrorAs(t, err, &gitError) + assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode) + assert.Equal(t, tt.wantError.Stderr, gitError.Stderr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantPushDefault, pushDefault) + }) + } +} + func TestClientDeleteLocalTag(t *testing.T) { tests := []struct { name string diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 57db40a851c..ce98a93638f 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -59,17 +59,7 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { remotesFn: factory.Remotes, httpClient: factory.HttpClient, pushDefault: func() (string, error) { - pushDefault, err := factory.GitClient.Config(context.Background(), "push.default") - if err == nil { - return pushDefault, nil - } - - var gitErr *git.GitError - if ok := errors.As(err, &gitErr); ok && gitErr.ExitCode == 1 { - return "simple", nil - } - - return "", err + return factory.GitClient.PushDefault(context.Background()) }, progress: factory.IOStreams, branchConfig: func(s string) (git.BranchConfig, error) { From e4d8ed0e6091ef4cc5961d3ffe08a293a2a8420c Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 24 Jan 2025 10:20:04 -0800 Subject: [PATCH 0257/2338] Remove @{push} from branch config --- git/client.go | 21 +++--- git/client_test.go | 109 +++++++++++++++-------------- git/objects.go | 3 +- pkg/cmd/pr/shared/finder.go | 33 +++++---- pkg/cmd/pr/shared/finder_test.go | 115 +++++++++++++++++-------------- 5 files changed, 151 insertions(+), 130 deletions(-) diff --git a/git/client.go b/git/client.go index c1951f07b76..c188b27bb1c 100644 --- a/git/client.go +++ b/git/client.go @@ -408,15 +408,10 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon return BranchConfig{}, err } - // Check to see if we can resolve the @{push} revision syntax. This is the easiest way to get - // the name of the push remote. - // We ignore errors resolving simple push.Default settings as these are handled downstream - revParseOut, _ := c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}") - - return parseBranchConfig(outputLines(branchCfgOut), strings.TrimSuffix(remotePushDefaultOut, "\n"), firstLine(revParseOut)), nil + return parseBranchConfig(outputLines(branchCfgOut), strings.TrimSuffix(remotePushDefaultOut, "\n")), nil } -func parseBranchConfig(branchConfigLines []string, remotePushDefault string, revParse string) BranchConfig { +func parseBranchConfig(branchConfigLines []string, remotePushDefault string) BranchConfig { var cfg BranchConfig // Read the config lines for the specific branch @@ -460,9 +455,6 @@ func parseBranchConfig(branchConfigLines []string, remotePushDefault string, rev } } - // The value returned by revParse is the easiest way to get the name of the push ref - cfg.Push = revParse - // Some `gh pr` workflows don't work if this is set to 'simple' or 'current' cfg.RemotePushDefault = remotePushDefault @@ -497,6 +489,15 @@ func (c *Client) PushDefault(ctx context.Context) (string, error) { return "", err } +// ParsePushRevision gets the value of the @{push} revision syntax +// An error here doesn't necessarily mean something are broke, but may mean that the @{push} +// revision syntax couldn't be resolved, such as in non-centralized workflows with +// push.default = simple. Downstream consumers should consider how to handle this error. +func (c *Client) ParsePushRevision(ctx context.Context, branch string) (string, error) { + revParseOut, err := c.revParse(ctx, "--abbrev-ref", branch+"@{push}") + return firstLine(revParseOut), err +} + func (c *Client) DeleteLocalTag(ctx context.Context, tag string) error { args := []string{"tag", "-d", tag} cmd, err := c.Command(ctx, args...) diff --git a/git/client_test.go b/git/client_test.go index 4e35879d94c..5d8ee9d6521 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -760,7 +760,7 @@ func TestClientReadBranchConfig(t *testing.T) { }, }, { - name: "when the git reads the config, pushDefault isn't set, and rev-parse succeeds, it should return the correct BranchConfig", + name: "when the git reads the config, pushDefault isn't set, it should return the correct BranchConfig", cmds: mockedCommands{ `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { Stdout: "branch.trunk.remote origin\n", @@ -768,20 +768,16 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { ExitStatus: 1, }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ RemoteName: "origin", PushRemoteName: "origin", - Push: "origin/trunk", }, wantError: nil, }, { - name: "when the git reads the config, pushDefault is set, and rev-parse succeeds, it should return the correct BranchConfig", + name: "when the git reads the config, pushDefault is set, it should return the correct BranchConfig", cmds: mockedCommands{ `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { Stdout: "branch.trunk.remote origin\n", @@ -789,15 +785,11 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { Stdout: "remotePushDefault", }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ RemoteName: "origin", PushRemoteName: "remotePushDefault", - Push: "origin/trunk", RemotePushDefault: "remotePushDefault", }, wantError: nil, @@ -811,9 +803,6 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { Stdout: "remotePushDefault", }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - ExitStatus: 1, - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ @@ -850,9 +839,6 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { ExitStatus: 1, }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ @@ -860,7 +846,6 @@ func TestClientReadBranchConfig(t *testing.T) { MergeRef: "refs/heads/trunk", MergeBase: "merge-base", PushRemoteName: "origin", - Push: "origin/trunk", }, wantError: nil, }, @@ -873,16 +858,12 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { ExitStatus: 1, }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk-remote", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ RemoteName: "origin", MergeRef: "refs/heads/trunk-remote", PushRemoteName: "origin", - Push: "origin/trunk-remote", }, }, { @@ -894,16 +875,12 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { ExitStatus: 1, }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ RemoteName: "origin", MergeRef: "refs/heads/main", PushRemoteName: "origin", - Push: "origin/trunk", }, }, { @@ -915,16 +892,12 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { ExitStatus: 1, }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin", - Push: "origin/trunk", }, }, { @@ -936,16 +909,12 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { Stdout: "origin", }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - Stdout: "origin/trunk", - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin", - Push: "origin/trunk", RemotePushDefault: "origin", }, }, @@ -958,9 +927,6 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { Stdout: "current", }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - ExitStatus: 1, - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ @@ -979,9 +945,6 @@ func TestClientReadBranchConfig(t *testing.T) { `path/to/git config remote.pushDefault`: { Stdout: "origin", }, - `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`: { - ExitStatus: 1, - }, }, branch: "trunk", wantBranchConfig: BranchConfig{ @@ -1018,7 +981,6 @@ func Test_parseBranchConfig(t *testing.T) { name string configLines []string pushDefault string - revParse string wantBranchConfig BranchConfig }{ { @@ -1050,14 +1012,6 @@ func Test_parseBranchConfig(t *testing.T) { PushRemoteName: "pushremote", }, }, - { - name: "rev parse specified", - configLines: []string{}, - revParse: "origin/trunk", - wantBranchConfig: BranchConfig{ - Push: "origin/trunk", - }, - }, { name: "push default specified", configLines: []string{}, @@ -1108,13 +1062,11 @@ func Test_parseBranchConfig(t *testing.T) { "branch.trunk.merge refs/heads/trunk", }, pushDefault: "remotePushDefault", - revParse: "origin/trunk", wantBranchConfig: BranchConfig{ RemoteName: "remote", PushRemoteName: "pushremote", MergeBase: "gh-merge-base", MergeRef: "refs/heads/trunk", - Push: "origin/trunk", RemotePushDefault: "remotePushDefault", }, }, @@ -1131,12 +1083,11 @@ func Test_parseBranchConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - branchConfig := parseBranchConfig(tt.configLines, tt.pushDefault, tt.revParse) + branchConfig := parseBranchConfig(tt.configLines, tt.pushDefault) assert.Equalf(t, tt.wantBranchConfig.RemoteName, branchConfig.RemoteName, "unexpected RemoteName") assert.Equalf(t, tt.wantBranchConfig.MergeRef, branchConfig.MergeRef, "unexpected MergeRef") assert.Equalf(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase, "unexpected MergeBase") assert.Equalf(t, tt.wantBranchConfig.PushRemoteName, branchConfig.PushRemoteName, "unexpected PushRemoteName") - assert.Equalf(t, tt.wantBranchConfig.Push, branchConfig.Push, "unexpected Push") assert.Equalf(t, tt.wantBranchConfig.RemotePushDefault, branchConfig.RemotePushDefault, "unexpected RemotePushDefault") if tt.wantBranchConfig.RemoteURL != nil { assert.Equalf(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String(), "unexpected RemoteURL") @@ -1253,6 +1204,60 @@ func TestClientPushDefault(t *testing.T) { } } +func TestClientParsePushRevision(t *testing.T) { + tests := []struct { + name string + branch string + commandResult commandResult + wantParsedPushRevision string + wantError *GitError + }{ + { + name: "@{push} resolves to origin/branchName", + branch: "branchName", + commandResult: commandResult{ + ExitStatus: 0, + Stdout: "origin/branchName", + }, + wantParsedPushRevision: "origin/branchName", + }, + { + name: "@{push} doesn't resolve", + commandResult: commandResult{ + ExitStatus: 128, + Stderr: "fatal: git error", + }, + wantParsedPushRevision: "", + wantError: &GitError{ + ExitCode: 128, + Stderr: "fatal: git error", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := fmt.Sprintf("path/to/git rev-parse --abbrev-ref %s@{push}", tt.branch) + cmdCtx := createMockedCommandContext(t, mockedCommands{ + args(cmd): tt.commandResult, + }) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + pushDefault, err := client.ParsePushRevision(context.Background(), tt.branch) + if tt.wantError != nil { + var gitError *GitError + require.ErrorAs(t, err, &gitError) + assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode) + assert.Equal(t, tt.wantError.Stderr, gitError.Stderr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantParsedPushRevision, pushDefault) + }) + } +} + func TestClientDeleteLocalTag(t *testing.T) { tests := []struct { name string diff --git a/git/objects.go b/git/objects.go index c6176fb73d3..eab7583aef7 100644 --- a/git/objects.go +++ b/git/objects.go @@ -68,8 +68,7 @@ type BranchConfig struct { MergeRef string // These are used to handle triangular workflows. They can be defined by either // a remote.pushDefault or a branch..pushremote value set on the git config. - RemotePushDefault string PushRemoteURL *url.URL PushRemoteName string - Push string + RemotePushDefault string } diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index ce98a93638f..d76a6214065 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -33,13 +33,14 @@ type progressIndicator interface { } type finder struct { - baseRepoFn func() (ghrepo.Interface, error) - branchFn func() (string, error) - remotesFn func() (remotes.Remotes, error) - httpClient func() (*http.Client, error) - pushDefault func() (string, error) - branchConfig func(string) (git.BranchConfig, error) - progress progressIndicator + baseRepoFn func() (ghrepo.Interface, error) + branchFn func() (string, error) + remotesFn func() (remotes.Remotes, error) + httpClient func() (*http.Client, error) + pushDefault func() (string, error) + parsePushRevision func(string) (string, error) + branchConfig func(string) (git.BranchConfig, error) + progress progressIndicator baseRefRepo ghrepo.Interface prNumber int @@ -61,6 +62,9 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { pushDefault: func() (string, error) { return factory.GitClient.PushDefault(context.Background()) }, + parsePushRevision: func(branch string) (string, error) { + return factory.GitClient.ParsePushRevision(context.Background(), branch) + }, progress: factory.IOStreams, branchConfig: func(s string) (git.BranchConfig, error) { return factory.GitClient.ReadBranchConfig(context.Background(), s) @@ -213,7 +217,10 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } - prRefs, err := parsePRRefs(f.branchName, branchConfig, pushDefault, f.baseRefRepo, rems) + // Suppressing the error as we have other means of computing the PRRefs if this fails. + parsedPushRevision, _ := f.parsePushRevision(f.branchName) + + prRefs, err := parsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, f.baseRefRepo, rems) if err != nil { return nil, nil, err } @@ -280,18 +287,18 @@ func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) { return repo, prNumber, nil } -func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, pushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { +func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { prRefs := PRRefs{ BaseRepo: baseRefRepo, } // If @{push} resolves, then we have all the information we need to determine the head repo // and branch name. It is of the form /. - if branchConfig.Push != "" { + if parsedPushRevision != "" { for _, r := range rems { // Find the remote who's name matches the push prefix - if strings.HasPrefix(branchConfig.Push, r.Name+"/") { - prRefs.BranchName = strings.TrimPrefix(branchConfig.Push, r.Name+"/") + if strings.HasPrefix(parsedPushRevision, r.Name+"/") { + prRefs.BranchName = strings.TrimPrefix(parsedPushRevision, r.Name+"/") prRefs.HeadRepo = r.Repo return prRefs, nil } @@ -301,7 +308,7 @@ func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, pushDe for i, r := range rems { remoteNames[i] = r.Name } - return PRRefs{}, fmt.Errorf("no remote for %q found in %q", branchConfig.Push, strings.Join(remoteNames, ", ")) + return PRRefs{}, fmt.Errorf("no remote for %q found in %q", parsedPushRevision, strings.Join(remoteNames, ", ")) } // To get the HeadRepo, we look to the git config. The PushRemote{Name | URL} comes from diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index fe01e6b6ec3..db56ce775f3 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -16,13 +16,14 @@ import ( ) type args struct { - baseRepoFn func() (ghrepo.Interface, error) - branchFn func() (string, error) - branchConfig func(string) (git.BranchConfig, error) - pushDefault func() (string, error) - selector string - fields []string - baseBranch string + baseRepoFn func() (ghrepo.Interface, error) + branchFn func() (string, error) + branchConfig func(string) (git.BranchConfig, error) + pushDefault func() (string, error) + parsePushRevision func(string) (string, error) + selector string + fields []string + baseBranch string } func TestFind(t *testing.T) { @@ -100,7 +101,8 @@ func TestFind(t *testing.T) { branchConfig: stubBranchConfig(git.BranchConfig{ PushRemoteName: remoteOrigin.Remote.Name, }, nil), - pushDefault: stubPushDefault("simple", nil), + pushDefault: stubPushDefault("simple", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -212,8 +214,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -254,8 +257,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -295,8 +299,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -328,8 +333,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -379,7 +385,8 @@ func TestFind(t *testing.T) { MergeRef: "refs/heads/blue-upstream-berries", PushRemoteName: "upstream", }, nil), - pushDefault: stubPushDefault("upstream", nil), + pushDefault: stubPushDefault("upstream", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -413,7 +420,8 @@ func TestFind(t *testing.T) { MergeRef: "refs/heads/blue-upstream-berries", PushRemoteURL: remoteUpstream.Remote.FetchURL, }, nil), - pushDefault: stubPushDefault("upstream", nil), + pushDefault: stubPushDefault("upstream", nil), + parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -443,10 +451,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{ - Push: "other/blueberries", - }, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + parsePushRevision: stubParsedPushRevision("other/blueberries", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -567,10 +574,11 @@ func TestFind(t *testing.T) { httpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, - baseRepoFn: tt.args.baseRepoFn, - branchFn: tt.args.branchFn, - branchConfig: tt.args.branchConfig, - pushDefault: tt.args.pushDefault, + baseRepoFn: tt.args.baseRepoFn, + branchFn: tt.args.branchFn, + branchConfig: tt.args.branchConfig, + pushDefault: tt.args.pushDefault, + parsePushRevision: tt.args.parsePushRevision, remotesFn: stubRemotes(context.Remotes{ &remoteOrigin, &remoteOther, @@ -641,14 +649,15 @@ func Test_parsePRRefs(t *testing.T) { } tests := []struct { - name string - branchConfig git.BranchConfig - pushDefault string - currentBranchName string - baseRefRepo ghrepo.Interface - rems context.Remotes - wantPRRefs PRRefs - wantErr error + name string + branchConfig git.BranchConfig + pushDefault string + parsedPushRevision string + currentBranchName string + baseRefRepo ghrepo.Interface + rems context.Remotes + wantPRRefs PRRefs + wantErr error }{ { name: "When the branch is called 'blueberries' with an empty branch config, it returns the correct PRRefs", @@ -675,12 +684,10 @@ func Test_parsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the branch name doesn't match the branch name in BranchConfig.Push, it returns the BranchConfig.Push branch name", - branchConfig: git.BranchConfig{ - Push: "origin/pushBranch", - }, - currentBranchName: "blueberries", - baseRefRepo: remoteOrigin.Repo, + name: "When the branch name doesn't match the branch name in BranchConfig.Push, it returns the BranchConfig.Push branch name", + parsedPushRevision: "origin/pushBranch", + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, rems: context.Remotes{ &remoteOrigin, }, @@ -692,12 +699,10 @@ func Test_parsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the push revision doesn't match a remote, it returns an error", - branchConfig: git.BranchConfig{ - Push: "origin/differentPushBranch", - }, - currentBranchName: "blueberries", - baseRefRepo: remoteOrigin.Repo, + name: "When the push revision doesn't match a remote, it returns an error", + parsedPushRevision: "origin/differentPushBranch", + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, rems: context.Remotes{ &remoteUpstream, &remoteOther, @@ -706,12 +711,10 @@ func Test_parsePRRefs(t *testing.T) { wantErr: fmt.Errorf("no remote for %q found in %q", "origin/differentPushBranch", "upstream, other"), }, { - name: "When the branch name doesn't match a different branch name in BranchConfig.Push and the remote isn't 'origin', it returns the BranchConfig.Push branch name", - branchConfig: git.BranchConfig{ - Push: "other/pushBranch", - }, - currentBranchName: "blueberries", - baseRefRepo: remoteOrigin.Repo, + name: "When the branch name doesn't match a different branch name in BranchConfig.Push and the remote isn't 'origin', it returns the BranchConfig.Push branch name", + parsedPushRevision: "other/pushBranch", + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, rems: context.Remotes{ &remoteOther, }, @@ -819,7 +822,7 @@ func Test_parsePRRefs(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.pushDefault, tt.baseRefRepo, tt.rems) + prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.parsedPushRevision, tt.pushDefault, tt.baseRefRepo, tt.rems) if tt.wantErr != nil { require.Equal(t, tt.wantErr, err) } else { @@ -887,3 +890,9 @@ func stubPushDefault(pushDefault string, err error) func() (string, error) { return pushDefault, err } } + +func stubParsedPushRevision(parsedPushRevision string, err error) func(string) (string, error) { + return func(_ string) (string, error) { + return parsedPushRevision, err + } +} From cdead50d572c4b9f830873f18724ac07afe4d3e4 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 24 Jan 2025 11:05:15 -0800 Subject: [PATCH 0258/2338] Moved remote.pushDefault out of ReadBranchConfig and into finder --- git/client.go | 46 ++--- git/client_test.go | 306 +++++++++---------------------- git/objects.go | 5 +- pkg/cmd/pr/shared/finder.go | 46 +++-- pkg/cmd/pr/shared/finder_test.go | 95 +++++++++- 5 files changed, 222 insertions(+), 276 deletions(-) diff --git a/git/client.go b/git/client.go index c188b27bb1c..be747e96242 100644 --- a/git/client.go +++ b/git/client.go @@ -402,16 +402,10 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon return BranchConfig{}, nil } - // Check to see if there is a pushDefault ref set for the repo - remotePushDefaultOut, err := c.Config(ctx, "remote.pushDefault") - if ok := errors.As(err, &gitError); ok && gitError.ExitCode != 1 { - return BranchConfig{}, err - } - - return parseBranchConfig(outputLines(branchCfgOut), strings.TrimSuffix(remotePushDefaultOut, "\n")), nil + return parseBranchConfig(outputLines(branchCfgOut)), nil } -func parseBranchConfig(branchConfigLines []string, remotePushDefault string) BranchConfig { +func parseBranchConfig(branchConfigLines []string) BranchConfig { var cfg BranchConfig // Read the config lines for the specific branch @@ -438,26 +432,6 @@ func parseBranchConfig(branchConfigLines []string, remotePushDefault string) Bra } } - // PushRemote{URL|Name} takes precedence over remotePushDefault, so we'll only - // use remotePushDefault if we don't have a push remote. - if cfg.PushRemoteURL == nil && cfg.PushRemoteName == "" { - // remotePushDefault usually indicates a "triangular" workflow - if remotePushDefault != "" { - pushRemoteURL, pushRemoteName := parseRemoteURLOrName(remotePushDefault) - cfg.PushRemoteURL = pushRemoteURL - cfg.PushRemoteName = pushRemoteName - } else { - // Without a PushRemote{URL|Name} or a remotePushDefault, we assume that the - // push remote ref is the same as the remote ref. This is likely - // a "centralized" workflow. - cfg.PushRemoteName = cfg.RemoteName - cfg.PushRemoteURL = cfg.RemoteURL - } - } - - // Some `gh pr` workflows don't work if this is set to 'simple' or 'current' - cfg.RemotePushDefault = remotePushDefault - return cfg } @@ -489,6 +463,22 @@ func (c *Client) PushDefault(ctx context.Context) (string, error) { return "", err } +// RemotePushDefault returns the value of remote.pushDefault in the config. If +// the value is not set, it returns an empty string. +func (c *Client) RemotePushDefault(ctx context.Context) (string, error) { + remotePushDefault, err := c.Config(ctx, "remote.pushDefault") + if err == nil { + return remotePushDefault, nil + } + + var gitError *GitError + if ok := errors.As(err, &gitError); ok && gitError.ExitCode == 1 { + return "", nil + } + + return "", err +} + // ParsePushRevision gets the value of the @{push} revision syntax // An error here doesn't necessarily mean something are broke, but may mean that the @{push} // revision syntax couldn't be resolved, such as in non-centralized workflows with diff --git a/git/client_test.go b/git/client_test.go index 5d8ee9d6521..99f78388113 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -760,200 +760,21 @@ func TestClientReadBranchConfig(t *testing.T) { }, }, { - name: "when the git reads the config, pushDefault isn't set, it should return the correct BranchConfig", + name: "when the config is read, it should return the correct BranchConfig", cmds: mockedCommands{ `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\n", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 1, + Stdout: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.pushremote origin\nbranch.trunk.gh-merge-base gh-merge-base\n", }, }, branch: "trunk", wantBranchConfig: BranchConfig{ - RemoteName: "origin", + RemoteName: "upstream", PushRemoteName: "origin", - }, - wantError: nil, - }, - { - name: "when the git reads the config, pushDefault is set, it should return the correct BranchConfig", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\n", - }, - `path/to/git config remote.pushDefault`: { - Stdout: "remotePushDefault", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "origin", - PushRemoteName: "remotePushDefault", - RemotePushDefault: "remotePushDefault", - }, - wantError: nil, - }, - { - name: "when git reads the config, pushDefault is set, an the branch hasn't been pushed, it should return the correct BranchConfig", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\n", - }, - `path/to/git config remote.pushDefault`: { - Stdout: "remotePushDefault", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "origin", - PushRemoteName: "remotePushDefault", - RemotePushDefault: "remotePushDefault", - }, - wantError: nil, - }, - { - name: "when git reads the config but remotePushDefault fails, it should return and empty BranchConfig and the error", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\n", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 2, - Stderr: "remotePushDefault error", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{}, - wantError: &GitError{ - ExitCode: 2, - Stderr: "remotePushDefault error", - }, - }, - { - name: "read branch config, central", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base merge-base", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 1, - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "origin", MergeRef: "refs/heads/trunk", - MergeBase: "merge-base", - PushRemoteName: "origin", + MergeBase: "gh-merge-base", }, wantError: nil, }, - { - name: "read branch config, central, push.default = upstream", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk-remote", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 1, - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "origin", - MergeRef: "refs/heads/trunk-remote", - PushRemoteName: "origin", - }, - }, - { - name: "read branch config, central, push.default = upstream, no existing remote branch", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/main", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 1, - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "origin", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - }, - }, - { - name: "read branch config, triangular, push.default = current, has existing remote branch, branch.trunk.pushremote effective", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main\nbranch.trunk.pushremote origin", - }, - `path/to/git config remote.pushDefault`: { - ExitStatus: 1, - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - }, - }, - { - name: "read branch config, triangular, push.default = current, has existing remote branch, remote.pushDefault effective", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main", - }, - `path/to/git config remote.pushDefault`: { - Stdout: "origin", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - RemotePushDefault: "origin", - }, - }, - { - name: "read branch config, triangular, push.default = current, no existing remote branch, branch.trunk.pushremote effective", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main\nbranch.trunk.pushremote origin", - }, - `path/to/git config remote.pushDefault`: { - Stdout: "current", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - RemotePushDefault: "current", - }, - }, - { - name: "read branch config, triangular, push.default = current, no existing remote branch, remote.pushDefault effective", - cmds: mockedCommands{ - `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main", - }, - `path/to/git config remote.pushDefault`: { - Stdout: "origin", - }, - }, - branch: "trunk", - wantBranchConfig: BranchConfig{ - RemoteName: "upstream", - MergeRef: "refs/heads/main", - PushRemoteName: "origin", - RemotePushDefault: "origin", - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -980,15 +801,13 @@ func Test_parseBranchConfig(t *testing.T) { tests := []struct { name string configLines []string - pushDefault string wantBranchConfig BranchConfig }{ { name: "remote branch", configLines: []string{"branch.trunk.remote origin"}, wantBranchConfig: BranchConfig{ - RemoteName: "origin", - PushRemoteName: "origin", + RemoteName: "origin", }, }, { @@ -1006,92 +825,73 @@ func Test_parseBranchConfig(t *testing.T) { }, }, { - name: "push remote", + name: "pushremote", configLines: []string{"branch.trunk.pushremote pushremote"}, wantBranchConfig: BranchConfig{ PushRemoteName: "pushremote", }, }, - { - name: "push default specified", - configLines: []string{}, - pushDefault: "remotePushDefault", - wantBranchConfig: BranchConfig{ - PushRemoteName: "remotePushDefault", - RemotePushDefault: "remotePushDefault", - }, - }, { name: "remote and pushremote are specified by name", configLines: []string{ - "branch.trunk.remote origin", - "branch.trunk.pushremote pushremote", + "branch.trunk.remote upstream", + "branch.trunk.pushremote origin", }, wantBranchConfig: BranchConfig{ - RemoteName: "origin", - PushRemoteName: "pushremote", + RemoteName: "upstream", + PushRemoteName: "origin", }, }, { name: "remote and pushremote are specified by url", configLines: []string{ - "branch.Frederick888/main.remote git@github.com:Frederick888/remote.git", - "branch.Frederick888/main.pushremote git@github.com:Frederick888/pushremote.git", + "branch.trunk.remote git@github.com:UPSTREAMOWNER/REPO.git", + "branch.trunk.pushremote git@github.com:ORIGINOWNER/REPO.git", }, wantBranchConfig: BranchConfig{ RemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", - Path: "/Frederick888/remote.git", + Path: "/UPSTREAMOWNER/REPO.git", }, PushRemoteURL: &url.URL{ Scheme: "ssh", User: url.User("git"), Host: "github.com", - Path: "/Frederick888/pushremote.git", + Path: "/ORIGINOWNER/REPO.git", }, }, }, { - name: "remote, pushremote, gh-merge-base, merge ref, push default, and rev parse all specified", + name: "remote, pushremote, gh-merge-base, and merge ref all specified", configLines: []string{ "branch.trunk.remote remote", "branch.trunk.pushremote pushremote", "branch.trunk.gh-merge-base gh-merge-base", "branch.trunk.merge refs/heads/trunk", }, - pushDefault: "remotePushDefault", - wantBranchConfig: BranchConfig{ - RemoteName: "remote", - PushRemoteName: "pushremote", - MergeBase: "gh-merge-base", - MergeRef: "refs/heads/trunk", - RemotePushDefault: "remotePushDefault", - }, - }, - { - name: "pushremote and pushDefault are not specified, but a remoteName is provided", - configLines: []string{ - "branch.trunk.remote remote", - }, wantBranchConfig: BranchConfig{ RemoteName: "remote", - PushRemoteName: "remote", + PushRemoteName: "pushremote", + MergeBase: "gh-merge-base", + MergeRef: "refs/heads/trunk", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - branchConfig := parseBranchConfig(tt.configLines, tt.pushDefault) + branchConfig := parseBranchConfig(tt.configLines) assert.Equalf(t, tt.wantBranchConfig.RemoteName, branchConfig.RemoteName, "unexpected RemoteName") assert.Equalf(t, tt.wantBranchConfig.MergeRef, branchConfig.MergeRef, "unexpected MergeRef") assert.Equalf(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase, "unexpected MergeBase") assert.Equalf(t, tt.wantBranchConfig.PushRemoteName, branchConfig.PushRemoteName, "unexpected PushRemoteName") - assert.Equalf(t, tt.wantBranchConfig.RemotePushDefault, branchConfig.RemotePushDefault, "unexpected RemotePushDefault") if tt.wantBranchConfig.RemoteURL != nil { assert.Equalf(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String(), "unexpected RemoteURL") } + if tt.wantBranchConfig.PushRemoteURL != nil { + assert.Equalf(t, tt.wantBranchConfig.PushRemoteURL.String(), branchConfig.PushRemoteURL.String(), "unexpected PushRemoteURL") + } }) } } @@ -1204,6 +1004,68 @@ func TestClientPushDefault(t *testing.T) { } } +func TestClientRemotePushDefault(t *testing.T) { + tests := []struct { + name string + commandResult commandResult + wantRemotePushDefault string + wantError *GitError + }{ + { + name: "remote.pushDefault is not set", + commandResult: commandResult{ + ExitStatus: 1, + Stderr: "error: key does not contain a section: remote.pushDefault", + }, + wantRemotePushDefault: "", + wantError: nil, + }, + { + name: "remote.pushDefault is set to origin", + commandResult: commandResult{ + ExitStatus: 0, + Stdout: "origin", + }, + wantRemotePushDefault: "origin", + wantError: nil, + }, + { + name: "remote.pushDefault errors", + commandResult: commandResult{ + ExitStatus: 128, + Stderr: "fatal: git error", + }, + wantRemotePushDefault: "", + wantError: &GitError{ + ExitCode: 128, + Stderr: "fatal: git error", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdCtx := createMockedCommandContext(t, mockedCommands{ + `path/to/git config remote.pushDefault`: tt.commandResult, + }, + ) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + pushDefault, err := client.RemotePushDefault(context.Background()) + if tt.wantError != nil { + var gitError *GitError + require.ErrorAs(t, err, &gitError) + assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode) + assert.Equal(t, tt.wantError.Stderr, gitError.Stderr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantRemotePushDefault, pushDefault) + }) + } +} + func TestClientParsePushRevision(t *testing.T) { tests := []struct { name string diff --git a/git/objects.go b/git/objects.go index eab7583aef7..d66d5b9bb2c 100644 --- a/git/objects.go +++ b/git/objects.go @@ -68,7 +68,6 @@ type BranchConfig struct { MergeRef string // These are used to handle triangular workflows. They can be defined by either // a remote.pushDefault or a branch..pushremote value set on the git config. - PushRemoteURL *url.URL - PushRemoteName string - RemotePushDefault string + PushRemoteURL *url.URL + PushRemoteName string } diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index d76a6214065..8589840a922 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -38,6 +38,7 @@ type finder struct { remotesFn func() (remotes.Remotes, error) httpClient func() (*http.Client, error) pushDefault func() (string, error) + remotePushDefault func() (string, error) parsePushRevision func(string) (string, error) branchConfig func(string) (git.BranchConfig, error) progress progressIndicator @@ -62,6 +63,9 @@ func NewFinder(factory *cmdutil.Factory) PRFinder { pushDefault: func() (string, error) { return factory.GitClient.PushDefault(context.Background()) }, + remotePushDefault: func() (string, error) { + return factory.GitClient.RemotePushDefault(context.Background()) + }, parsePushRevision: func(branch string) (string, error) { return factory.GitClient.ParsePushRevision(context.Background(), branch) }, @@ -217,10 +221,15 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } - // Suppressing the error as we have other means of computing the PRRefs if this fails. + // Suppressing these errors as we have other means of computing the PRRefs when these fail. parsedPushRevision, _ := f.parsePushRevision(f.branchName) - prRefs, err := parsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, f.baseRefRepo, rems) + remotePushDefault, err := f.remotePushDefault() + if err != nil { + return nil, nil, err + } + + prRefs, err := parsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, f.baseRefRepo, rems) if err != nil { return nil, nil, err } @@ -287,7 +296,7 @@ func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) { return repo, prNumber, nil } -func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { +func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, remotePushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { prRefs := PRRefs{ BaseRepo: baseRefRepo, } @@ -311,8 +320,15 @@ func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsed return PRRefs{}, fmt.Errorf("no remote for %q found in %q", parsedPushRevision, strings.Join(remoteNames, ", ")) } - // To get the HeadRepo, we look to the git config. The PushRemote{Name | URL} comes from - // one of the following, in order of precedence: + // We assume the PR's branch name is the same as whatever f.BranchFn() returned earlier + // unless the user has specified push.default = upstream or tracking, then we use the + // branch name from the merge ref. + prRefs.BranchName = currentBranchName + if pushDefault == "upstream" || pushDefault == "tracking" { + prRefs.BranchName = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") + } + + // To get the HeadRepo, we look to the git config. The HeadRepo comes from one of the following, in order of precedence: // 1. branch..pushRemote // 2. remote.pushDefault // 3. branch..remote @@ -324,14 +340,18 @@ func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsed if r, err := ghrepo.FromURL(branchConfig.PushRemoteURL); err == nil { prRefs.HeadRepo = r } - } - - // We assume the PR's branch name is the same as whatever f.BranchFn() returned earlier. - // unless the user has specified push.default = upstream or tracking, then we use the - // branch name from the merge ref. - prRefs.BranchName = currentBranchName - if pushDefault == "upstream" || pushDefault == "tracking" { - prRefs.BranchName = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") + } else if remotePushDefault != "" { + if r, err := rems.FindByName(remotePushDefault); err == nil { + prRefs.HeadRepo = r.Repo + } + } else if branchConfig.RemoteName != "" { + if r, err := rems.FindByName(branchConfig.RemoteName); err == nil { + prRefs.HeadRepo = r.Repo + } + } else if branchConfig.RemoteURL != nil { + if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil { + prRefs.HeadRepo = r + } } // The PR merges from a branch in the same repo as the base branch (usually the default branch) diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index db56ce775f3..89835b0289f 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -20,6 +20,7 @@ type args struct { branchFn func() (string, error) branchConfig func(string) (git.BranchConfig, error) pushDefault func() (string, error) + remotePushDefault func() (string, error) parsePushRevision func(string) (string, error) selector string fields []string @@ -102,6 +103,7 @@ func TestFind(t *testing.T) { PushRemoteName: remoteOrigin.Remote.Name, }, nil), pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { @@ -132,8 +134,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), }, wantErr: true, }, @@ -154,8 +157,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), }, httpStub: nil, wantPR: 13, @@ -170,8 +174,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -192,8 +197,9 @@ func TestFind(t *testing.T) { branchFn: func() (string, error) { return "blueberries", nil }, - branchConfig: stubBranchConfig(git.BranchConfig{}, nil), - pushDefault: stubPushDefault("simple", nil), + branchConfig: stubBranchConfig(git.BranchConfig{}, nil), + pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -217,6 +223,7 @@ func TestFind(t *testing.T) { branchConfig: stubBranchConfig(git.BranchConfig{}, nil), pushDefault: stubPushDefault("simple", nil), parsePushRevision: stubParsedPushRevision("", nil), + remotePushDefault: stubRemotePushDefault("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -259,6 +266,7 @@ func TestFind(t *testing.T) { }, branchConfig: stubBranchConfig(git.BranchConfig{}, nil), pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { @@ -301,6 +309,7 @@ func TestFind(t *testing.T) { }, branchConfig: stubBranchConfig(git.BranchConfig{}, nil), pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { @@ -335,6 +344,7 @@ func TestFind(t *testing.T) { }, branchConfig: stubBranchConfig(git.BranchConfig{}, nil), pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { @@ -386,6 +396,7 @@ func TestFind(t *testing.T) { PushRemoteName: "upstream", }, nil), pushDefault: stubPushDefault("upstream", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { @@ -421,6 +432,7 @@ func TestFind(t *testing.T) { PushRemoteURL: remoteUpstream.Remote.FetchURL, }, nil), pushDefault: stubPushDefault("upstream", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("", nil), }, httpStub: func(r *httpmock.Registry) { @@ -453,6 +465,7 @@ func TestFind(t *testing.T) { }, branchConfig: stubBranchConfig(git.BranchConfig{}, nil), pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), parsePushRevision: stubParsedPushRevision("other/blueberries", nil), }, httpStub: func(r *httpmock.Registry) { @@ -513,7 +526,8 @@ func TestFind(t *testing.T) { branchConfig: stubBranchConfig(git.BranchConfig{ MergeRef: "refs/pull/13/head", }, nil), - pushDefault: stubPushDefault("simple", nil), + pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", nil), }, httpStub: func(r *httpmock.Registry) { r.Register( @@ -578,6 +592,7 @@ func TestFind(t *testing.T) { branchFn: tt.args.branchFn, branchConfig: tt.args.branchConfig, pushDefault: tt.args.pushDefault, + remotePushDefault: tt.args.remotePushDefault, parsePushRevision: tt.args.parsePushRevision, remotesFn: stubRemotes(context.Remotes{ &remoteOrigin, @@ -653,6 +668,7 @@ func Test_parsePRRefs(t *testing.T) { branchConfig git.BranchConfig pushDefault string parsedPushRevision string + remotePushDefault string currentBranchName string baseRefRepo ghrepo.Interface rems context.Remotes @@ -819,10 +835,63 @@ func Test_parsePRRefs(t *testing.T) { }, wantErr: nil, }, + { + name: "When remote.pushDefault is set, it returns the correct PRRefs", + branchConfig: git.BranchConfig{}, + remotePushDefault: remoteUpstream.Remote.Name, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteUpstream.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the remote name is set on the branch, it returns the correct PRRefs", + branchConfig: git.BranchConfig{ + RemoteName: remoteUpstream.Remote.Name, + }, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteUpstream.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, + { + name: "When the remote URL is set on the branch, it returns the correct PRRefs", + branchConfig: git.BranchConfig{ + RemoteURL: remoteUpstream.Remote.FetchURL, + }, + currentBranchName: "blueberries", + baseRefRepo: remoteOrigin.Repo, + rems: context.Remotes{ + &remoteOrigin, + &remoteUpstream, + }, + wantPRRefs: PRRefs{ + BranchName: "blueberries", + HeadRepo: remoteUpstream.Repo, + BaseRepo: remoteOrigin.Repo, + }, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.parsedPushRevision, tt.pushDefault, tt.baseRefRepo, tt.rems) + prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.parsedPushRevision, tt.pushDefault, tt.remotePushDefault, tt.baseRefRepo, tt.rems) if tt.wantErr != nil { require.Equal(t, tt.wantErr, err) } else { @@ -891,6 +960,12 @@ func stubPushDefault(pushDefault string, err error) func() (string, error) { } } +func stubRemotePushDefault(remotePushDefault string, err error) func() (string, error) { + return func() (string, error) { + return remotePushDefault, err + } +} + func stubParsedPushRevision(parsedPushRevision string, err error) func(string) (string, error) { return func(_ string) (string, error) { return parsedPushRevision, err From d684834ad9728ff5e8bb5db57d869864c6188170 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 24 Jan 2025 11:51:49 -0800 Subject: [PATCH 0259/2338] Refactor pr status to use the ParsePRRefs helper on the Finder There was a lot of copy-pasta code between the finder and pr status. After some investigation it was clear that the prSelectorForCurrentBranch code was really just a duplicate of the finder's code without actually making the API call for the PR. Since the ParsePRRefs helper had already extracted much of the logic for determining a PR's head ref branch, I was able to reuse it in gh pr status with a small refactor. --- pkg/cmd/pr/shared/finder.go | 4 +- pkg/cmd/pr/shared/finder_test.go | 4 +- pkg/cmd/pr/status/status.go | 110 +++++------- pkg/cmd/pr/status/status_test.go | 292 +++---------------------------- 4 files changed, 70 insertions(+), 340 deletions(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 8589840a922..ae0ea45534d 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -229,7 +229,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } - prRefs, err := parsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, f.baseRefRepo, rems) + prRefs, err := ParsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, f.baseRefRepo, rems) if err != nil { return nil, nil, err } @@ -296,7 +296,7 @@ func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) { return repo, prNumber, nil } -func parsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, remotePushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { +func ParsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, remotePushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { prRefs := PRRefs{ BaseRepo: baseRefRepo, } diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 89835b0289f..1d5c1e3ac7f 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -631,7 +631,7 @@ func TestFind(t *testing.T) { } } -func Test_parsePRRefs(t *testing.T) { +func TestParsePRRefs(t *testing.T) { originOwnerUrl, err := url.Parse("https://github.com/ORIGINOWNER/REPO.git") if err != nil { t.Fatal(err) @@ -891,7 +891,7 @@ func Test_parsePRRefs(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - prRefs, err := parsePRRefs(tt.currentBranchName, tt.branchConfig, tt.parsedPushRevision, tt.pushDefault, tt.remotePushDefault, tt.baseRefRepo, tt.rems) + prRefs, err := ParsePRRefs(tt.currentBranchName, tt.branchConfig, tt.parsedPushRevision, tt.pushDefault, tt.remotePushDefault, tt.baseRefRepo, tt.rems) if tt.wantErr != nil { require.Equal(t, tt.wantErr, err) } else { diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 89926bfa222..996a178e1c7 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -7,7 +7,6 @@ import ( "net/http" "regexp" "strconv" - "strings" "time" "github.com/cli/cli/v2/api" @@ -78,27 +77,56 @@ func statusRun(opts *StatusOptions) error { return err } - baseRepo, err := opts.BaseRepo() + baseRefRepo, err := opts.BaseRepo() if err != nil { return err } - var currentBranch string + var currentBranchName string var currentPRNumber int - var currentPRHeadRef string + var currentHeadRefBranchName string if !opts.HasRepoOverride { - currentBranch, err = opts.Branch() + currentBranchName, err = opts.Branch() if err != nil && !errors.Is(err, git.ErrNotOnAnyBranch) { return fmt.Errorf("could not query for pull request for current branch: %w", err) } - remotes, _ := opts.Remotes() - branchConfig, err := opts.GitClient.ReadBranchConfig(ctx, currentBranch) + branchConfig, err := opts.GitClient.ReadBranchConfig(ctx, currentBranchName) if err != nil { return err } - currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(branchConfig, baseRepo, currentBranch, remotes) + // Determine if the branch is configured to merge to a special PR ref + prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`) + if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil { + currentPRNumber, _ = strconv.Atoi(m[1]) + } + + if currentPRNumber == 0 { + remotes, err := opts.Remotes() + if err != nil { + return err + } + // Suppressing these errors as we have other means of computing the PRRefs when these fail. + parsedPushRevision, _ := opts.GitClient.ParsePushRevision(ctx, currentBranchName) + + remotePushDefault, err := opts.GitClient.RemotePushDefault(ctx) + if err != nil { + return err + } + + pushDefault, err := opts.GitClient.PushDefault(ctx) + if err != nil { + return err + } + + prRefs, err := shared.ParsePRRefs(currentBranchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, baseRefRepo, remotes) + if err != nil { + return err + } + currentHeadRefBranchName = prRefs.BranchName + } + if err != nil { return fmt.Errorf("could not query for pull request for current branch: %w", err) } @@ -107,7 +135,7 @@ func statusRun(opts *StatusOptions) error { options := requestOptions{ Username: "@me", CurrentPR: currentPRNumber, - HeadRef: currentPRHeadRef, + HeadRef: currentHeadRefBranchName, ConflictStatus: opts.ConflictStatus, } if opts.Exporter != nil { @@ -116,7 +144,7 @@ func statusRun(opts *StatusOptions) error { if opts.Detector == nil { cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) - opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + opts.Detector = fd.NewDetector(cachedClient, baseRefRepo.RepoHost()) } prFeatures, err := opts.Detector.PullRequestFeatures() if err != nil { @@ -124,7 +152,7 @@ func statusRun(opts *StatusOptions) error { } options.CheckRunAndStatusContextCountsSupported = prFeatures.CheckRunAndStatusContextCounts - prPayload, err := pullRequestStatus(httpClient, baseRepo, options) + prPayload, err := pullRequestStatus(httpClient, baseRefRepo, options) if err != nil { return err } @@ -151,21 +179,21 @@ func statusRun(opts *StatusOptions) error { cs := opts.IO.ColorScheme() fmt.Fprintln(out, "") - fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(baseRepo)) + fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(baseRefRepo)) fmt.Fprintln(out, "") if !opts.HasRepoOverride { shared.PrintHeader(opts.IO, "Current branch") currentPR := prPayload.CurrentPR - if currentPR != nil && currentPR.State != "OPEN" && prPayload.DefaultBranch == currentBranch { + if currentPR != nil && currentPR.State != "OPEN" && prPayload.DefaultBranch == currentBranchName { currentPR = nil } if currentPR != nil { printPrs(opts.IO, 1, *currentPR) - } else if currentPRHeadRef == "" { + } else if currentHeadRefBranchName == "" { shared.PrintMessage(opts.IO, " There is no current branch") } else { - shared.PrintMessage(opts.IO, fmt.Sprintf(" There is no pull request associated with %s", cs.Cyan("["+currentPRHeadRef+"]"))) + shared.PrintMessage(opts.IO, fmt.Sprintf(" There is no pull request associated with %s", cs.Cyan("["+currentHeadRefBranchName+"]"))) } fmt.Fprintln(out) } @@ -189,58 +217,6 @@ func statusRun(opts *StatusOptions) error { return nil } -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, err := strconv.Atoi(m[1]) - if err != nil { - return 0, "", err - } - return prNumber, prHeadRef, nil - } - var branchOwner string - if branchConfig.PushRemoteURL != nil { - // the branch merges from a remote specified by URL - r, err := ghrepo.FromURL(branchConfig.PushRemoteURL) - 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.PushRemoteName != "" { - // the branch merges from a remote specified by name - r, err := rem.FindByName(branchConfig.PushRemoteName) - 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 branchConfig.Push != "" { - // If we have a resolved push revision ref, we defer to that - selector = strings.TrimPrefix(branchConfig.Push, branchConfig.PushRemoteName+"/") - } else if (branchConfig.RemotePushDefault == "upstream" || branchConfig.RemotePushDefault == "tracking") && - strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { - selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") - } - // prepend `OWNER:` if this branch is pushed to a fork - if !strings.EqualFold(branchOwner, baseRepo.RepoOwner()) { - selector = fmt.Sprintf("%s:%s", branchOwner, selector) - } - return 0, selector, nil - } - - return 0, prHeadRef, nil -} - func totalApprovals(pr *api.PullRequest) int { approvals := 0 for _, review := range pr.LatestReviews.Nodes { diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 4e9ca19b0ea..c55604c2843 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -4,7 +4,6 @@ import ( "bytes" "io" "net/http" - "net/url" "regexp" "strings" "testing" @@ -96,12 +95,13 @@ 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 + // stub successful git commands rs, cleanup := run.Stub() defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -133,7 +133,8 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -165,7 +166,8 @@ func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{}) if err != nil { @@ -196,7 +198,8 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -231,7 +234,8 @@ func TestPRStatus_currentBranch_defaultBranch(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -272,7 +276,8 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -296,7 +301,8 @@ func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -320,7 +326,8 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -344,7 +351,8 @@ func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -368,7 +376,8 @@ func TestPRStatus_blankSlate(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "blueberries", true, "") if err != nil { @@ -428,7 +437,8 @@ func TestPRStatus_detachedHead(t *testing.T) { defer cleanup(t) rs.Register(`git config --get-regexp \^branch\\.`, 0, "") rs.Register(`git config remote.pushDefault`, 0, "") - rs.Register(`git rev-parse --verify --quiet --abbrev-ref @{push}`, 0, "") + rs.Register(`git rev-parse --abbrev-ref @{push}`, 0, "") + rs.Register(`git config push.default`, 0, "") output, err := runCommand(http, "", true, "") if err != nil { @@ -461,259 +471,3 @@ func TestPRStatus_error_ReadBranchConfig(t *testing.T) { _, err := runCommand(initFakeHTTP(), "blueberries", true, "") assert.Error(t, err) } - -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{ - PushRemoteURL: &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{ - PushRemoteURL: &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{ - PushRemoteURL: &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, - }, - { - name: "Current branch pushes to default upstream", - branchConfig: git.BranchConfig{ - PushRemoteURL: &url.URL{ - Scheme: "ssh", - User: url.User("git"), - Host: "github.com", - Path: "Frederick888/playground.git", - }, - MergeRef: "refs/heads/main", - RemotePushDefault: "upstream", - }, - baseRepo: ghrepo.NewWithHost("octocat", "playground", "github.com"), - prHeadRef: "Frederick888/main", - remotes: context.Remotes{ - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.NewWithHost("octocat", "playground", "github.com"), - }, - }, - wantPrNumber: 0, - wantSelector: "Frederick888:main", - wantError: nil, - }, - { - name: "Current branch pushes to default tracking", - branchConfig: git.BranchConfig{ - PushRemoteURL: &url.URL{ - Scheme: "ssh", - User: url.User("git"), - Host: "github.com", - Path: "Frederick888/playground.git", - }, - MergeRef: "refs/heads/main", - RemotePushDefault: "tracking", - }, - baseRepo: ghrepo.NewWithHost("octocat", "playground", "github.com"), - prHeadRef: "Frederick888/main", - remotes: context.Remotes{ - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.NewWithHost("octocat", "playground", "github.com"), - }, - }, - wantPrNumber: 0, - wantSelector: "Frederick888:main", - wantError: nil, - }, - } - - 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) - }) - } -} From 11dc8d48f54e068ee8775f16faaaaf34c4fb515c Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 13:19:47 -0700 Subject: [PATCH 0260/2338] reorder fields Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 3ac642904b6..b5389043100 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -35,8 +35,8 @@ type Options struct { SignerRef string SignerRepo string SignerWorkflow string - SourceRef string SourceDigest string + SourceRef string APIClient api.Client Logger *io.Handler OCIClient oci.Client From 728aa3d83fbc30d7978af82f31652cf9f81ae300 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 13:20:01 -0700 Subject: [PATCH 0261/2338] set new options in enforcement criteria Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy.go | 38 ++++++++++++++++++++++- pkg/cmd/attestation/verify/policy_test.go | 14 +++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 1e1dcd4d892..bb6ddb27863 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -14,6 +14,7 @@ import ( ) const hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$` +const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/[a-zA-Z0-9-]+.(yml|yaml)$` func expandToGitHubURL(tenant, ownerOrRepo string) string { if tenant == "" { @@ -66,7 +67,7 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er // then we default to the repo option c.SANRegex = expandToGitHubURLRegex(opts.Tenant, opts.Repo) } else { - // if opts.Repo was not provided, we fallback to the opts.Owner value + // if opts.Repo was not provided, we fall back to the opts.Owner value c.SANRegex = expandToGitHubURLRegex(opts.Tenant, owner) } @@ -98,9 +99,44 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er c.Certificate.Issuer = opts.OIDCIssuer } + if opts.SignerDigest != "" { + c.Certificate.BuildSignerDigest = opts.SignerDigest + } + + if opts.SignerRef != "" { + // need to build the full URI value + uri, err := getFullWorkflowURI(c.SANRegex) + if err != nil { + return verification.EnforcementCriteria{}, err + } + c.Certificate.BuildSignerURI = uri + } + + if opts.SourceDigest != "" { + c.Certificate.SourceRepositoryDigest = opts.SourceDigest + } + + if opts.SourceRef != "" { + c.Certificate.SourceRepositoryRef = opts.SourceRef + } + return c, nil } +func getFullWorkflowURI(s string) (string, error) { + trimmed, _ := strings.CutPrefix(s, "^") + match, err := regexp.MatchString(workflowURIRegex, trimmed) + if err != nil { + return "", err + } + + if !match { + return "", nil + } + + return trimmed, nil +} + func buildCertificateIdentityOption(c verification.EnforcementCriteria) (verify.PolicyOption, error) { sanMatcher, err := verify.NewSANMatcher(c.SAN, c.SANRegex) if err != nil { diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index d033ba4fabd..ecaf74ec361 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -1,6 +1,7 @@ package verify import ( + "fmt" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" @@ -275,3 +276,16 @@ func TestValidateSignerWorkflow(t *testing.T) { } } } + +func TestGetFullWorkflowURI(t *testing.T) { + expectedURI := "https://github.com/foo/bar/.github/workflows/mybuildjob.yaml" + // exact matching + uri, err := getFullWorkflowURI(expectedURI) + require.NoError(t, err) + require.Equal(t, expectedURI, uri) + + // matching after stripping regex prefix characters + uri, err = getFullWorkflowURI(fmt.Sprintf("^%s", expectedURI)) + require.NoError(t, err) + require.Equal(t, expectedURI, uri) +} From b582b5830bda851626c79c86e59d3f756a5c9576 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 24 Jan 2025 16:24:00 -0500 Subject: [PATCH 0262/2338] Upgrade sigstore-go to v0.7.0: fixes #10114 formatting issue Signed-off-by: Cody Soyland --- go.mod | 30 +++++++++++++++--------------- go.sum | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ca554958724..d8110c247cc 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( 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.3 - github.com/sigstore/sigstore-go v0.6.2 + github.com/sigstore/sigstore-go v0.7.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 @@ -50,7 +50,7 @@ require ( golang.org/x/sync v0.10.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/grpc v1.69.4 google.golang.org/protobuf v1.36.4 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 @@ -93,7 +93,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/certificate-transparency-go v1.2.1 // indirect + github.com/google/certificate-transparency-go v1.3.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect @@ -121,7 +121,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -130,21 +130,21 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect - github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect - github.com/sigstore/rekor v1.3.6 // indirect - github.com/sigstore/sigstore v1.8.9 // indirect - github.com/sigstore/timestamp-authority v1.2.2 // indirect + github.com/sigstore/rekor v1.3.8 // indirect + github.com/sigstore/sigstore v1.8.12 // indirect + github.com/sigstore/timestamp-authority v1.2.4 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/viper v1.18.2 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/viper v1.19.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect - github.com/theupdateframework/go-tuf/v2 v2.0.1 // indirect + github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect 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 @@ -158,13 +158,13 @@ require ( 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/exp v0.0.0-20240325151524-a685a6edb6d8 // 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.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 + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect gopkg.in/ini.v1 v1.67.0 // indirect - k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/go.sum b/go.sum index 5a7147b193e..65791ea73ec 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,7 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= @@ -42,30 +43,40 @@ github.com/aws/aws-sdk-go v1.51.6 h1:Ld36dn9r7P9IjU8WZSaswQ8Y/XUCRpewim5980DwYiU github.com/aws/aws-sdk-go v1.51.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo= github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk= github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c= +github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg= github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c= github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA= github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 h1:yS0JkEdV6h9JOo8sy2JSpjX+i7vsKifU8SIeHrqiDhU= github.com/aws/aws-sdk-go-v2/service/kms v1.30.0/go.mod h1:+I8VUUSVD4p5ISQtzpgSva4I8cJ4SQ4b1dcBcof7O+g= github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw= github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw= github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk= github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM= 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= @@ -203,6 +214,7 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME= github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= 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.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= @@ -345,6 +357,7 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+ 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= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -384,6 +397,7 @@ github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgm github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= +github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= @@ -396,20 +410,27 @@ github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQF 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/rekor v1.3.8/go.mod h1:/dHFYKSuxEygfDRnEwyJ+ZD6qoVYNXQdi1mJrKvKWsI= github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= +github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo= github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A= github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0= +github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48= +github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3/go.mod h1:G4+I83FILPX6MtnoaUdmv/bRGEVtR3JdLeJa/kXdk/0= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY= github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 h1:vDl2fqPT0h3D/k6NZPlqnKFd1tz3335wm39qjvpZNJc= github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3/go.mod h1:9uOJXbXEXj+M6QjMKH5PaL5WDMu43rHfbIMgXzA8eKI= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3 h1:h9G8j+Ds21zqqulDbA/R/ft64oQQIyp8S7wJYABYSlg= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3/go.mod h1:zgCeHOuqF6k7A7TTEvftcA9V3FRzB7mrPtHOhXAQBnc= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12 h1:jvY1B9bjP+tKzdKDyuq5K7O19CG2IKzGJNTy5tuL2Gs= 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/sigstore/timestamp-authority v1.2.4/go.mod h1:ExrbobKdEuwuBptZIiKp1IaVBRiUeKbiuSyZTO8Okik= 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= @@ -418,12 +439,14 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -435,6 +458,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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= @@ -443,6 +467,7 @@ github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qv github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= github.com/theupdateframework/go-tuf/v2 v2.0.1 h1:11p9tXpq10KQEujxjcIjDSivMKCMLguls7erXHZnxJQ= github.com/theupdateframework/go-tuf/v2 v2.0.1/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= +github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= @@ -479,6 +504,7 @@ go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qq 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.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -491,6 +517,7 @@ 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/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= @@ -537,14 +564,18 @@ 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= +google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s= google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U= google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -561,6 +592,7 @@ gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= From d60c4fbd7bc7322144d1993532950a63ffc3e525 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 24 Jan 2025 16:26:49 -0500 Subject: [PATCH 0263/2338] go mod tidy Signed-off-by: Cody Soyland --- go.mod | 2 - go.sum | 238 ++++++++++++++++++++++++++------------------------------- 2 files changed, 110 insertions(+), 130 deletions(-) diff --git a/go.mod b/go.mod index d8110c247cc..50111817487 100644 --- a/go.mod +++ b/go.mod @@ -98,8 +98,6 @@ require ( 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 - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 65791ea73ec..fea9b24189d 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,36 @@ -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= +cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= 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= -cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A= +cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -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/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 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= @@ -39,46 +45,36 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.51.6 h1:Ld36dn9r7P9IjU8WZSaswQ8Y/XUCRpewim5980DwYiU= -github.com/aws/aws-sdk-go v1.51.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= -github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo= -github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk= -github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c= +github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= +github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg= github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= +github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA= -github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 h1:yS0JkEdV6h9JOo8sy2JSpjX+i7vsKifU8SIeHrqiDhU= -github.com/aws/aws-sdk-go-v2/service/kms v1.30.0/go.mod h1:+I8VUUSVD4p5ISQtzpgSva4I8cJ4SQ4b1dcBcof7O+g= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 h1:KbLZjYqhQ9hyB4HwXiheiflTlYQa0+Fz0Ms/rh5f3mk= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.8/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y= github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM= -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/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 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= @@ -91,12 +87,10 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= -github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= @@ -206,14 +200,11 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME= -github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE= +github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -221,20 +212,20 @@ github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM 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= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= -github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4= -github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5FjV2mCWI= +github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek= +github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -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/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 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= @@ -246,8 +237,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= @@ -267,8 +256,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE= -github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE= +github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= +github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -287,12 +276,22 @@ github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI= github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8= github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= -github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= -github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= +github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -345,6 +344,8 @@ github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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= @@ -355,8 +356,7 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ 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= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= @@ -365,14 +365,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0= github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -395,8 +395,7 @@ github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGq github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= -github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= -github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= +github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -408,28 +407,21 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= 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/rekor v1.3.8 h1:B8kJI8mpSIXova4Jxa6vXdJyysRxFGsEsLKBDl0rRjA= github.com/sigstore/rekor v1.3.8/go.mod h1:/dHFYKSuxEygfDRnEwyJ+ZD6qoVYNXQdi1mJrKvKWsI= -github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= -github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= +github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc= github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo= -github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A= -github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0= github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48= github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3/go.mod h1:G4+I83FILPX6MtnoaUdmv/bRGEVtR3JdLeJa/kXdk/0= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12/go.mod h1:aw60vs3crnQdM/DYH+yF2P0MVKtItwAX34nuaMrY7Lk= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 h1:vDl2fqPT0h3D/k6NZPlqnKFd1tz3335wm39qjvpZNJc= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3/go.mod h1:9uOJXbXEXj+M6QjMKH5PaL5WDMu43rHfbIMgXzA8eKI= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3 h1:h9G8j+Ds21zqqulDbA/R/ft64oQQIyp8S7wJYABYSlg= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3/go.mod h1:zgCeHOuqF6k7A7TTEvftcA9V3FRzB7mrPtHOhXAQBnc= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12/go.mod h1:NkPiz4XA0JcBSXzJUrjMj7Xi7oSTew1Ip3Zmt56mHlw= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12 h1:kweBChR6M9FEvmxN3BMEcl7SNnwxTwKF7THYFKLOE5U= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12/go.mod h1:6+d+A6oYt1W5OgtzgEVb21V7tAZ/C2Ihtzc5MNJbayY= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12 h1:jvY1B9bjP+tKzdKDyuq5K7O19CG2IKzGJNTy5tuL2Gs= -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/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12/go.mod h1:2uEeOb8xE2RC6OvzxKux1wkS39Zv8gA27z92m49xUTc= +github.com/sigstore/timestamp-authority v1.2.4 h1:RjXZxOWorEiem/uSr0pFHVtQpyzpcFxgugo5jVqm3mw= github.com/sigstore/timestamp-authority v1.2.4/go.mod h1:ExrbobKdEuwuBptZIiKp1IaVBRiUeKbiuSyZTO8Okik= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -437,15 +429,13 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -465,8 +455,7 @@ 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= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= -github.com/theupdateframework/go-tuf/v2 v2.0.1 h1:11p9tXpq10KQEujxjcIjDSivMKCMLguls7erXHZnxJQ= -github.com/theupdateframework/go-tuf/v2 v2.0.1/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= +github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if370jjzO84SS+Ebo= github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= @@ -486,12 +475,10 @@ github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8L 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/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= 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= @@ -500,11 +487,12 @@ go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5W 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/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 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.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU= +go.step.sm/crypto v0.57.0/go.mod h1:+Lwp5gOVPaTa3H/Ul/TzGbxQPXZZcKIUGMS0lG6n9Go= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -515,8 +503,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= @@ -554,27 +541,23 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.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.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= google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= -google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s= -google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U= -google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +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= @@ -590,8 +573,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 6a629ed87137fd408d106a15f057a94b500be582 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 24 Jan 2025 16:35:31 -0500 Subject: [PATCH 0264/2338] Fix breaking changes from sigstore-go v0.7.0 upgrade Signed-off-by: Cody Soyland --- pkg/cmd/attestation/inspect/inspect.go | 2 +- pkg/cmd/attestation/verification/sigstore.go | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 81c37588d2b..b2d0adfb747 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -178,7 +178,7 @@ func runInspect(opts *Options) error { } // summarize cert if present - if leafCert := verificationContent.GetCertificate(); leafCert != nil { + if leafCert := verificationContent.Certificate(); leafCert != nil { certSummary, err := certificate.SummarizeCertificate(leafCert) if err != nil { diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 4f94666e5a8..c71c600c78f 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -70,7 +70,7 @@ func getBundleIssuer(b *bundle.Bundle) (string, error) { if err != nil { return "", fmt.Errorf("failed to get bundle verification content: %v", err) } - leafCert := verifyContent.GetCertificate() + leafCert := verifyContent.Certificate() if leafCert == nil { return "", fmt.Errorf("leaf cert not found") } @@ -116,7 +116,11 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti // Compare bundle leafCert issuer with trusted root cert authority certAuthorities := trustedRoot.FulcioCertificateAuthorities() for _, certAuthority := range certAuthorities { - lowestCert, err := getLowestCertInChain(&certAuthority) + fulcioCertAuthority, ok := certAuthority.(*root.FulcioCertificateAuthority) + if !ok { + return nil, fmt.Errorf("trusted root cert authority is not a FulcioCertificateAuthority") + } + lowestCert, err := getLowestCertInChain(fulcioCertAuthority) if err != nil { return nil, err } @@ -149,10 +153,8 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti return nil, fmt.Errorf("unable to use provided trusted roots") } -func getLowestCertInChain(ca *root.CertificateAuthority) (*x509.Certificate, error) { - if ca.Leaf != nil { - return ca.Leaf, nil - } else if len(ca.Intermediates) > 0 { +func getLowestCertInChain(ca *root.FulcioCertificateAuthority) (*x509.Certificate, error) { + if len(ca.Intermediates) > 0 { return ca.Intermediates[0], nil } else if ca.Root != nil { return ca.Root, nil From 4382bdf072b558790358abda47b8cf3d802cd895 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 24 Jan 2025 14:01:44 -0800 Subject: [PATCH 0265/2338] Fix pr create tests Due to the refactor of BranchConfig, the tests for `pr create` no longer require a handful of stubbed git commands. Additionally, I noticed some overlap between `pr create`'s existing logic with the new finder code. I suspect that the finder's new ParsePRRefs helper could be leveraged here as well, which would allow us to respect non-centralized workflows pr create in the same way we are respecting them in `gh pr view` and `gh pr status` --- pkg/cmd/pr/create/create.go | 5 +- pkg/cmd/pr/create/create_test.go | 87 -------------------------------- pkg/cmd/pr/shared/finder.go | 6 +++ 3 files changed, 10 insertions(+), 88 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 124bd4b0724..6c9d2a2aff3 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,6 +518,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } +// TODO: Replace with the finder's PRRefs struct // trackingRef represents a ref for a remote tracking branch. type trackingRef struct { remoteName string @@ -685,7 +686,9 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { return nil, err } if isPushEnabled { - // determine whether the head branch is already pushed to a remote + // TODO: This doesn't respect the @{push} revision resolution or triagular workflows assembled with + // remote.pushDefault, or branch..pushremote config settings. The finder's ParsePRRefs + // may be able to replace this function entirely. if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found { isPushEnabled = false if r, err := remotes.FindByName(trackingRef.remoteName); err == nil { diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 6629b2fa1e8..c486997ad3e 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -353,8 +353,6 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", @@ -384,10 +382,6 @@ func Test_createRun(t *testing.T) { opts.HeadBranch = "feature" return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", }, { @@ -402,10 +396,6 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, expectedOutputs: []string{ "Would have created a Pull Request with:", `title: my title`, @@ -437,10 +427,6 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), @@ -501,10 +487,6 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, expectedOutputs: []string{ `Would have created a Pull Request with:`, `Title: my title`, @@ -542,10 +524,6 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), @@ -612,10 +590,6 @@ func Test_createRun(t *testing.T) { opts.DryRun = true return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, expectedOut: heredoc.Doc(` Would have created a Pull Request with: Title: TITLE @@ -662,8 +636,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, @@ -727,8 +699,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, @@ -775,8 +745,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, @@ -826,8 +794,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "") @@ -886,8 +852,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD deadb00f refs/remotes/upstream/feature @@ -925,8 +889,6 @@ func Test_createRun(t *testing.T) { branch.feature.remote origin branch.feature.merge refs/heads/my-feat2 `)) // determineTrackingBranch - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/my-feat2") cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD deadbeef refs/remotes/origin/my-feat2 @@ -967,8 +929,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "d3476a1\u0000commit 0\u0000\u0000\n7a6ea13\u0000commit 1\u0000\u0000") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1009,10 +969,6 @@ func Test_createRun(t *testing.T) { opts.Milestone = "big one.oh" return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), @@ -1100,10 +1056,6 @@ func Test_createRun(t *testing.T) { opts.Finder = shared.NewMockFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")) return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, wantErr: "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123", }, { @@ -1120,8 +1072,6 @@ func Test_createRun(t *testing.T) { httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -1154,8 +1104,6 @@ func Test_createRun(t *testing.T) { mockRetrieveProjects(t, reg) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -1203,8 +1151,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature`, 0, "") cs.Register(`git rev-parse --show-toplevel`, 0, "") }, @@ -1264,8 +1210,6 @@ func Test_createRun(t *testing.T) { })) }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1314,8 +1258,6 @@ func Test_createRun(t *testing.T) { { name: "web long URL", cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, setup: func(opts *CreateOptions, t *testing.T) func() { @@ -1342,10 +1284,6 @@ func Test_createRun(t *testing.T) { } return func() {} }, - cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") - }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), @@ -1365,8 +1303,6 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, @@ -1442,8 +1378,6 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, @@ -1480,8 +1414,6 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, @@ -1532,8 +1464,6 @@ func Test_createRun(t *testing.T) { return func() {} }, cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register("git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", @@ -1593,8 +1523,6 @@ func Test_createRun(t *testing.T) { deadbeef HEAD deadb00f refs/remotes/upstream/feature/feat2 deadbeef refs/remotes/origin/task1`)) // determineTrackingBranch - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref task1@\{push\}`, 1, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for monalisa:task1 into feature/feat2 in OWNER/REPO\n\n", @@ -1705,9 +1633,6 @@ 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 config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, headBranchConfig: git.BranchConfig{}, @@ -1717,9 +1642,6 @@ func Test_tryDetermineTrackingRef(t *testing.T) { { name: "no match", cmdStubs: func(cs *run.CommandStubber) { - cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") - cs.Register(`git config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 1, "") 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{}, @@ -1739,9 +1661,6 @@ 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 config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/feature") 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 @@ -1768,12 +1687,6 @@ 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 config remote.pushDefault`, 1, "") - cs.Register(`git rev-parse --verify --quiet --abbrev-ref feature@\{push\}`, 0, "origin/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 diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index ae0ea45534d..2059399312f 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -97,6 +97,12 @@ type FindOptions struct { States []string } +// TODO: Does this also need the BaseBranchName? +// PR's are represented by the following: +// baseRef -----PR-----> headRef +// +// A ref is described as "remoteName/branchName", so +// baseRepoName/baseBranchName -----PR-----> headRepoName/headBranchName type PRRefs struct { BranchName string HeadRepo ghrepo.Interface From 62106dc800ada6ed8d4c8deab6270c3c0a008d44 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 24 Jan 2025 14:18:20 -0800 Subject: [PATCH 0266/2338] Cleanup comment --- git/objects.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/git/objects.go b/git/objects.go index d66d5b9bb2c..b57d79becce 100644 --- a/git/objects.go +++ b/git/objects.go @@ -66,8 +66,7 @@ type BranchConfig struct { // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. MergeBase string MergeRef string - // These are used to handle triangular workflows. They can be defined by either - // a remote.pushDefault or a branch..pushremote value set on the git config. + // These are usually used to handle triangular workflows. PushRemoteURL *url.URL PushRemoteName string } From 532be61d6bcfca854670250f663cd8d400f8c31b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:35:50 +0000 Subject: [PATCH 0267/2338] Bump github.com/in-toto/attestation from 1.1.0 to 1.1.1 Bumps [github.com/in-toto/attestation](https://github.com/in-toto/attestation) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/in-toto/attestation/releases) - [Commits](https://github.com/in-toto/attestation/compare/v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: github.com/in-toto/attestation dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ca554958724..ba20dc0bd6d 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.1.4 - github.com/in-toto/attestation v1.1.0 + github.com/in-toto/attestation v1.1.1 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.14 diff --git a/go.sum b/go.sum index 5a7147b193e..9dbbcdcd79d 100644 --- a/go.sum +++ b/go.sum @@ -265,8 +265,8 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= -github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q= -github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs= +github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI= +github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= From 7c31d1a76b6c7a0e3eeb71ccb34b55b679e5fef5 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 27 Jan 2025 17:47:55 -0500 Subject: [PATCH 0268/2338] Minor refactoring autolink create help and logic - simplified and wrapped `gh repo autolink create` and `gh repo autolink` long help usage docs - simplified success message, brought into alignment with other commands --- pkg/cmd/repo/autolink/autolink.go | 14 ++--- pkg/cmd/repo/autolink/create/create.go | 69 +++++++-------------- pkg/cmd/repo/autolink/create/create_test.go | 13 +--- 3 files changed, 33 insertions(+), 63 deletions(-) diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index b988de60d0d..1fd2183543a 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -13,14 +13,12 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { Use: "autolink ", Short: "Manage autolink references", Long: heredoc.Docf(` - Work with GitHub autolink references. - - GitHub autolinks require admin access to configure and can be found at - https://github.com/{owner}/{repo}/settings/key_links. - Use %[1]sgh repo autolink list --web%[1]s to open this page for the current repository. - - For more information about GitHub autolinks, see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources - `, "`"), + Autolinks link issues, pull requests, commit messages, and release descriptions to external third-party services. + + Autolinks require %[1]sadmin%[1]s role to view or manage. + + For more information, see + `, "`"), } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index 6c321db5b16..ac3035c79e9 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -40,32 +40,35 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co Long: heredoc.Docf(` Create a new autolink reference for a repository. - Autolinks automatically generate links to external resources when they appear in an issue, pull request, or commit. + The %[1]skeyPrefix%[1]s argument specifies the prefix that will generate a link when it is appended by certain characters. - The %[1]skeyPrefix%[1]s specifies the prefix that will generate a link when it is appended by certain characters. + The %[1]surlTemplate%[1]s argument specifies the target URL that will be generated when the keyPrefix is found, which + must contain %[1]s%[1]s variable for the reference number. - The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The %[1]surlTemplate%[1]s must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. + By default, autolinks are alphanumeric with %[1]s--numeric%[1]s flag used to create a numeric autolink. - By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. + The %[1]s%[1]s variable behavior differs depending on whether the autolink is alphanumeric or numeric: - If you are using a shell that applies special meaning to angle brackets, you will need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around the whole argument. - - Only repository administrators can create autolinks. + - alphanumeric: matches %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s + - numeric: matches %[1]s0-9%[1]s + + If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. `, "`"), Example: heredoc.Doc(` - # Create an alphanumeric autolink to example.com for the key prefix "TICKET-". This will generate a link to https://example.com/TICKET?query=123abc when "TICKET-123abc" appears. - $ gh repo autolink create TICKET- https://example.com/TICKET?query= - - # Create a numeric autolink to example.com for the key prefix "STORY-". This will generate a link to https://example.com/STORY?id=123 when "STORY-123" appears. - $ gh repo autolink create STORY- https://example.com/STORY?id= --numeric + # Create an alphanumeric autolink to example.com for the key prefix "TICKET-". + # Generates https://example.com/TICKET?query=123abc from "TICKET-123abc". + $ gh repo autolink create TICKET- "https://example.com/TICKET?query=" + # Create a numeric autolink to example.com for the key prefix "STORY-". + # Generates https://example.com/STORY?id=123 from "STORY-123". + $ gh repo autolink create STORY- "https://example.com/STORY?id=" --numeric `), Args: cmdutil.ExactArgs(2, "Cannot create autolink: keyPrefix and urlTemplate arguments are both required"), Aliases: []string{"new"}, RunE: func(c *cobra.Command, args []string) error { opts.BaseRepo = f.BaseRepo - httpClient, err := f.HttpClient() + httpClient, err := f.HttpClient() if err != nil { return err } @@ -82,7 +85,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as non-alphanumeric") + cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as numeric") return cmd } @@ -92,48 +95,24 @@ func createRun(opts *createOptions) error { if err != nil { return err } - cs := opts.IO.ColorScheme() - - isAlphanumeric := !opts.Numeric request := AutolinkCreateRequest{ KeyPrefix: opts.KeyPrefix, URLTemplate: opts.URLTemplate, - IsAlphanumeric: isAlphanumeric, + IsAlphanumeric: !opts.Numeric, } autolink, err := opts.AutolinkClient.Create(repo, request) - if err != nil { - return fmt.Errorf("%s %w", cs.Red("error creating autolink:"), err) - } - - msg := successMsg(autolink, repo, cs) - fmt.Fprint(opts.IO.Out, msg) - - return nil -} - -func successMsg(autolink *shared.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { - autolinkType := "Numeric" - if autolink.IsAlphanumeric { - autolinkType = "Alphanumeric" + return fmt.Errorf("error creating autolink: %w", err) } - createdMsg := fmt.Sprintf( - "%s %s autolink created in %s (id: %d)", + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.Out, + "%s Created repository autolink %d on %s\n", cs.SuccessIconWithColor(cs.Green), - autolinkType, - ghrepo.FullName(repo), autolink.ID, - ) + ghrepo.FullName(repo)) - autolinkMapMsg := cs.Bluef( - " %s%s → %s", - autolink.KeyPrefix, - "", - autolink.URLTemplate, - ) - - return fmt.Sprintf("%s\n%s\n", createdMsg, autolinkMapMsg) + return nil } diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go index 43fe3e4708b..477d28da9e4 100644 --- a/pkg/cmd/repo/autolink/create/create_test.go +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -6,7 +6,6 @@ import ( "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/pkg/cmd/repo/autolink/shared" @@ -133,10 +132,7 @@ func TestCreateRun(t *testing.T) { URLTemplate: "https://example.com/TICKET?query=", }, stubCreator: stubAutoLinkCreator{}, - wantStdout: heredoc.Doc(` - ✓ Alphanumeric autolink created in OWNER/REPO (id: 1) - TICKET- → https://example.com/TICKET?query= - `), + wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n", }, { name: "success, numeric", @@ -146,10 +142,7 @@ func TestCreateRun(t *testing.T) { Numeric: true, }, stubCreator: stubAutoLinkCreator{}, - wantStdout: heredoc.Doc(` - ✓ Numeric autolink created in OWNER/REPO (id: 1) - TICKET- → https://example.com/TICKET?query= - `), + wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n", }, { name: "client error", @@ -180,7 +173,7 @@ func TestCreateRun(t *testing.T) { if tt.expectedErr != nil { require.Error(t, err) assert.ErrorIs(t, err, tt.expectedErr) - assert.Equal(t, err.Error(), tt.errMsg) + assert.Equal(t, tt.errMsg, err.Error()) } else { require.NoError(t, err) } From e31bfd0092e5482247c6a6a90ca1d8c4c6c7a966 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Mon, 27 Jan 2025 16:40:47 -0800 Subject: [PATCH 0269/2338] Cleaned up some naming and comments --- git/client.go | 20 +++++++------------- git/client_test.go | 20 +++++++++++++------- git/objects.go | 13 +++++++------ pkg/cmd/pr/shared/finder.go | 6 +++--- pkg/cmd/pr/shared/finder_test.go | 10 +++++++--- 5 files changed, 37 insertions(+), 32 deletions(-) diff --git a/git/client.go b/git/client.go index be747e96242..30cb4306d2e 100644 --- a/git/client.go +++ b/git/client.go @@ -389,13 +389,11 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon return BranchConfig{}, err } - // This is the error we expect if a git command does not run successfully. - // If the ExitCode is 1, then we just didn't find any config for the branch. - // We will use this error to check against the commands that are allowed to - // return an empty result. - var gitError *GitError branchCfgOut, 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 } @@ -417,14 +415,9 @@ func parseBranchConfig(branchConfigLines []string) BranchConfig { keys := strings.Split(parts[0], ".") switch keys[len(keys)-1] { case "remote": - remoteURL, remoteName := parseRemoteURLOrName(parts[1]) - cfg.RemoteURL = remoteURL - cfg.RemoteName = remoteName - // pushremote usually indicates a "triangular" workflow + cfg.RemoteURL, cfg.RemoteName = parseRemoteURLOrName(parts[1]) case "pushremote": - pushRemoteURL, pushRemoteName := parseRemoteURLOrName(parts[1]) - cfg.PushRemoteURL = pushRemoteURL - cfg.PushRemoteName = pushRemoteName + cfg.PushRemoteURL, cfg.PushRemoteName = parseRemoteURLOrName(parts[1]) case "merge": cfg.MergeRef = parts[1] case MergeBaseConfig: @@ -449,7 +442,8 @@ func (c *Client) SetBranchConfig(ctx context.Context, branch, name, value string } // PushDefault returns the value of push.default in the config. If the value -// is not set, it returns "simple" (the default value). +// is not set, it returns "simple" (the default git value). See +// https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault func (c *Client) PushDefault(ctx context.Context) (string, error) { pushDefault, err := c.Config(ctx, "push.default") if err == nil { diff --git a/git/client_test.go b/git/client_test.go index 99f78388113..872c8595b8a 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -14,6 +14,7 @@ import ( "strings" "testing" + "github.com/MakeNowJust/heredoc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -763,7 +764,12 @@ func TestClientReadBranchConfig(t *testing.T) { name: "when the config is read, it should return the correct BranchConfig", cmds: mockedCommands{ `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: { - Stdout: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.pushremote origin\nbranch.trunk.gh-merge-base gh-merge-base\n", + Stdout: heredoc.Doc(` + branch.trunk.remote upstream + branch.trunk.merge refs/heads/trunk + branch.trunk.pushremote origin + branch.trunk.gh-merge-base gh-merge-base + `), }, }, branch: "trunk", @@ -1869,16 +1875,16 @@ func TestCommandMocking(t *testing.T) { jsonVar, ok := os.LookupEnv("GH_HELPER_PROCESS_RICH_COMMANDS") if !ok { fmt.Fprint(os.Stderr, "missing GH_HELPER_PROCESS_RICH_COMMANDS") - // Exit 1 is reserved for empty command responses by git. This can be desirable in some cases, - // so returning an arbitrary exit code to avoid suppressing this if an exit code 1 is allowed. + // Exit 1 is used for empty key values in the git config. This is non-breaking in those use cases, + // so this is returning a non-zero exit code to avoid suppressing this error for those use cases. os.Exit(16) } var commands mockedCommands if err := json.Unmarshal([]byte(jsonVar), &commands); err != nil { fmt.Fprint(os.Stderr, "failed to unmarshal GH_HELPER_PROCESS_RICH_COMMANDS") - // Exit 1 is reserved for empty command responses by git. This can be desirable in some cases, - // so returning an arbitrary exit code to avoid suppressing this if an exit code 1 is allowed. + // Exit 1 is used for empty key values in the git config. This is non-breaking in those use cases, + // so this is returning a non-zero exit code to avoid suppressing this error for those use cases. os.Exit(16) } @@ -1888,8 +1894,8 @@ func TestCommandMocking(t *testing.T) { commandResult, ok := commands[args(strings.Join(realArgs, " "))] if !ok { fmt.Fprintf(os.Stderr, "unexpected command: %s\n", strings.Join(realArgs, " ")) - // Exit 1 is reserved for empty command responses by git. This can be desirable in some cases, - // so returning an arbitrary exit code to avoid suppressing this if an exit code 1 is allowed. + // Exit 1 is used for empty key values in the git config. This is non-breaking in those use cases, + // so this is returning a non-zero exit code to avoid suppressing this error for those use cases. os.Exit(16) } diff --git a/git/objects.go b/git/objects.go index b57d79becce..9db528b8c3c 100644 --- a/git/objects.go +++ b/git/objects.go @@ -60,13 +60,14 @@ type Commit struct { Body string } +// These are the keys we read from the git branch. config. type BranchConfig struct { - RemoteName string - RemoteURL *url.URL + RemoteName string // .remote if string + RemoteURL *url.URL // .remote if url + MergeRef string // .merge + PushRemoteName string // .pushremote if string + PushRemoteURL *url.URL // .pushremote if url + // MergeBase is the optional base branch to target in a new PR if `--base` is not specified. MergeBase string - MergeRef string - // These are usually used to handle triangular workflows. - PushRemoteURL *url.URL - PushRemoteName string } diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 2059399312f..05ec24c9ad0 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -109,10 +109,10 @@ type PRRefs struct { BaseRepo ghrepo.Interface } -// GetPRLabel returns the string that the GitHub API uses to identify the PR. This is +// GetPRHeadLabel returns the string that the GitHub API uses to identify the PR. This is // either just the branch name or, if the PR is originating from a fork, the fork owner // and the branch name, like :. -func (s *PRRefs) GetPRLabel() string { +func (s *PRRefs) GetPRHeadLabel() string { if ghrepo.IsSame(s.HeadRepo, s.BaseRepo) { return s.BranchName } @@ -240,7 +240,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } - pr, err = findForBranch(httpClient, f.baseRefRepo, opts.BaseBranch, prRefs.GetPRLabel(), opts.States, fields.ToSlice()) + pr, err = findForBranch(httpClient, f.baseRefRepo, opts.BaseBranch, prRefs.GetPRHeadLabel(), opts.States, fields.ToSlice()) if err != nil { return pr, f.baseRefRepo, err } diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 1d5c1e3ac7f..a54412db51b 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -419,7 +419,11 @@ func TestFind(t *testing.T) { wantRepo: "https://github.com/ORIGINOWNER/REPO", }, { - name: "the current branch is configured to push to and pull from a URL (upstream, in this example) that is different from what the repo is configured to push to and pull from (origin, in this example) and push.default = upstream, it finds the PR associated with the upstream repo and returns origin as the base repo", + // The current BRANCH is configured to push to and pull from a URL (upstream, in this example) + // which is different from what the REPO is configured to push to and pull from (origin, in this example) + // and push.default = upstream. It should find the PR associated with the upstream repo and return + // origin as the base repo + name: "when push.default = upstream and the current branch is configured to push/pull from a different remote than the repo", args: args{ selector: "", fields: []string{"id", "number"}, @@ -902,7 +906,7 @@ func TestParsePRRefs(t *testing.T) { } } -func TestPRRefs_GetPRLabel(t *testing.T) { +func TestPRRefs_GetPRHeadLabel(t *testing.T) { originRepo := ghrepo.New("ORIGINOWNER", "REPO") upstreamRepo := ghrepo.New("UPSTREAMOWNER", "REPO") tests := []struct { @@ -931,7 +935,7 @@ func TestPRRefs_GetPRLabel(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.prRefs.GetPRLabel()) + assert.Equal(t, tt.want, tt.prRefs.GetPRHeadLabel()) }) } } From 359b4a1d637e31e86e0645385b04f9c74c89b944 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 28 Jan 2025 17:18:30 +0500 Subject: [PATCH 0270/2338] Stop progress indicator on failure (`gh run list --workflow`) --- pkg/cmd/run/list/list.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 793bfbb856e..0b291fae530 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -131,6 +131,7 @@ func listRun(opts *ListOptions) error { filters.WorkflowID = workflow.ID filters.WorkflowName = workflow.Name } else { + opts.IO.StopProgressIndicator() return err } } From e8475d9fb8a3cd2193bbbecce3706a547dcdbd51 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sat, 11 Jan 2025 20:00:44 -0500 Subject: [PATCH 0271/2338] Initial, working implementation --- pkg/cmd/repo/autolink/autolink.go | 2 + pkg/cmd/repo/autolink/list/list.go | 9 +- pkg/cmd/repo/autolink/shared/autolink.go | 7 ++ pkg/cmd/repo/autolink/view/http.go | 47 ++++++++++ pkg/cmd/repo/autolink/view/view.go | 107 +++++++++++++++++++++++ 5 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 pkg/cmd/repo/autolink/view/http.go create mode 100644 pkg/cmd/repo/autolink/view/view.go diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index 1fd2183543a..09b766b8513 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -4,6 +4,7 @@ import ( "github.com/MakeNowJust/heredoc" cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create" 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" "github.com/spf13/cobra" ) @@ -24,6 +25,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)) return cmd } diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index a68c03f3c94..74267e7a414 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -15,13 +15,6 @@ import ( "github.com/spf13/cobra" ) -var autolinkFields = []string{ - "id", - "isAlphanumeric", - "keyPrefix", - "urlTemplate", -} - type listOptions struct { BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser @@ -70,7 +63,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman } cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List autolink references in the web browser") - cmdutil.AddJSONFlags(cmd, &opts.Exporter, autolinkFields) + cmdutil.AddJSONFlags(cmd, &opts.Exporter, domain.AutolinkFields) return cmd } diff --git a/pkg/cmd/repo/autolink/shared/autolink.go b/pkg/cmd/repo/autolink/shared/autolink.go index d79537c3184..28e975e7cf3 100644 --- a/pkg/cmd/repo/autolink/shared/autolink.go +++ b/pkg/cmd/repo/autolink/shared/autolink.go @@ -9,6 +9,13 @@ type Autolink struct { URLTemplate string `json:"url_template"` } +var AutolinkFields = []string{ + "id", + "isAlphanumeric", + "keyPrefix", + "urlTemplate", +} + func (a *Autolink) ExportData(fields []string) map[string]interface{} { return cmdutil.StructExportData(a, fields) } diff --git a/pkg/cmd/repo/autolink/view/http.go b/pkg/cmd/repo/autolink/view/http.go new file mode 100644 index 00000000000..d3fdfc86ad3 --- /dev/null +++ b/pkg/cmd/repo/autolink/view/http.go @@ -0,0 +1,47 @@ +package view + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" +) + +type AutolinkViewer struct { + HTTPClient *http.Client +} + +func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*domain.Autolink, 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.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := a.HTTPClient.Do(req) + if err != nil { + return nil, err + } + 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) + } else if resp.StatusCode > 299 { + return nil, api.HandleHTTPError(resp) + } + + var autolink domain.Autolink + err = json.NewDecoder(resp.Body).Decode(&autolink) + + if err != nil { + fmt.Println(err.Error()) + return nil, err + } + + return &autolink, nil +} diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go new file mode 100644 index 00000000000..2d0cfd6c65c --- /dev/null +++ b/pkg/cmd/repo/autolink/view/view.go @@ -0,0 +1,107 @@ +package view + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type viewOptions struct { + BaseRepo func() (ghrepo.Interface, error) + Browser browser.Browser + AutolinkClient AutolinkViewClient + IO *iostreams.IOStreams + Exporter cmdutil.Exporter + + ID string +} + +type AutolinkViewClient interface { + View(repo ghrepo.Interface, id string) (*domain.Autolink, error) +} + +func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Command { + opts := &viewOptions{ + Browser: f.Browser, + IO: f.IOStreams, + } + + cmd := &cobra.Command{ + Use: "view ", + Short: "View an autolink reference", + Long: heredoc.Docf(` + View an autolink reference for a repository. + + Information about autolinks is only available to repository administrators. + `), + 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.ID = args[0] + + if err != nil { + return err + } + + opts.AutolinkClient = &AutolinkViewer{HTTPClient: httpClient} + + if runF != nil { + return runF(opts) + } + + return viewRun(opts) + }, + } + + cmdutil.AddJSONFlags(cmd, &opts.Exporter, domain.AutolinkFields) + + return cmd +} + +func viewRun(opts *viewOptions) error { + repo, err := opts.BaseRepo() + if err != nil { + return err + } + cs := opts.IO.ColorScheme() + + autolink, err := opts.AutolinkClient.View(repo, opts.ID) + + if err != nil { + return fmt.Errorf("%s %w", cs.Red("error viewing autolink:"), err) + } + + if opts.Exporter != nil { + return opts.Exporter.Write(opts.IO, autolink) + } + + msg := heredoc.Docf(` + Autolink in %s + + ID: %d + Key Prefix: %s + URL Template: %s + Alphanumeric: %t + `, + ghrepo.FullName(repo), + autolink.ID, + autolink.KeyPrefix, + autolink.URLTemplate, + autolink.IsAlphanumeric, + ) + fmt.Fprint(opts.IO.Out, msg) + + return nil +} From f941608091c29411cafcfb5173d6f29ab5fab6d4 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 13 Jan 2025 09:01:12 -0500 Subject: [PATCH 0272/2338] Partial testing for view --- pkg/cmd/repo/autolink/view/view_test.go | 193 ++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 pkg/cmd/repo/autolink/view/view_test.go diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go new file mode 100644 index 00000000000..4ace02ea5a4 --- /dev/null +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -0,0 +1,193 @@ +package view + +import ( + "bytes" + "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/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/jsonfieldstest" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJSONFields(t *testing.T) { + jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdView, []string{ + "id", + "isAlphanumeric", + "keyPrefix", + "urlTemplate", + }) +} + +func TestNewCmdView(t *testing.T) { + tests := []struct { + name string + input string + output viewOptions + wantErr bool + wantExporter bool + errMsg string + }{ + { + name: "no argument", + input: "", + wantErr: true, + errMsg: "accepts 1 arg(s), received 0", + }, + { + name: "id provided", + input: "123", + output: viewOptions{ID: "123"}, + }, + { + name: "json flag", + input: "123 --json id", + output: viewOptions{}, + 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", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + 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 *viewOptions + cmd := NewCmdView(f, func(opts *viewOptions) 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.wantExporter, gotOpts.Exporter != nil) + } + }) + } +} + +type stubAutoLinkViewer struct { + autolink *domain.Autolink + err error +} + +func (g stubAutoLinkViewer) View(repo ghrepo.Interface, id string) (*domain.Autolink, error) { + return g.autolink, g.err +} + +type testAutolinkClientViewError struct{} + +func (e testAutolinkClientViewError) Error() string { + return "autolink client view error" +} + +func TestViewRun(t *testing.T) { + tests := []struct { + name string + opts *viewOptions + stubViewer stubAutoLinkViewer + expectedErr error + wantStdout string + wantStderr string + }{ + { + name: "view", + opts: &viewOptions{ + ID: "1", + }, + stubViewer: stubAutoLinkViewer{ + autolink: &domain.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + wantStdout: heredoc.Doc(` + Autolink in OWNER/REPO + + ID: 1 + Key Prefix: TICKET- + URL Template: https://example.com/TICKET?query= + Alphanumeric: true + `), + }, + { + name: "view json", + opts: &viewOptions{ + Exporter: func() cmdutil.Exporter { + exporter := cmdutil.NewJSONExporter() + exporter.SetFields([]string{"id"}) + return exporter + }(), + }, + stubViewer: stubAutoLinkViewer{ + autolink: &domain.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + wantStdout: "{\"id\":1}\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + + 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.AutolinkClient = &tt.stubViewer + err := viewRun(opts) + + if tt.expectedErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantStdout, stdout.String()) + } + + if tt.wantStderr != "" { + assert.Equal(t, tt.wantStderr, stderr.String()) + } + }) + } +} From 11ce0c661c494fd3ef356a6de4f898934802b48e Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 13 Jan 2025 09:10:12 -0500 Subject: [PATCH 0273/2338] Updates following renaming domain to shared --- pkg/cmd/repo/autolink/list/list.go | 2 +- pkg/cmd/repo/autolink/view/http.go | 6 +++--- pkg/cmd/repo/autolink/view/view.go | 6 +++--- pkg/cmd/repo/autolink/view/view_test.go | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 74267e7a414..123c5321e02 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -63,7 +63,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman } cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List autolink references in the web browser") - cmdutil.AddJSONFlags(cmd, &opts.Exporter, domain.AutolinkFields) + cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.AutolinkFields) return cmd } diff --git a/pkg/cmd/repo/autolink/view/http.go b/pkg/cmd/repo/autolink/view/http.go index d3fdfc86ad3..ddba23f6de7 100644 --- a/pkg/cmd/repo/autolink/view/http.go +++ b/pkg/cmd/repo/autolink/view/http.go @@ -8,14 +8,14 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" ) type AutolinkViewer struct { HTTPClient *http.Client } -func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*domain.Autolink, error) { +func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, 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.MethodGet, url, nil) @@ -35,7 +35,7 @@ func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*domain.Autolin return nil, api.HandleHTTPError(resp) } - var autolink domain.Autolink + var autolink shared.Autolink err = json.NewDecoder(resp.Body).Decode(&autolink) if err != nil { diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go index 2d0cfd6c65c..38a2590a275 100644 --- a/pkg/cmd/repo/autolink/view/view.go +++ b/pkg/cmd/repo/autolink/view/view.go @@ -6,7 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "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/spf13/cobra" @@ -23,7 +23,7 @@ type viewOptions struct { } type AutolinkViewClient interface { - View(repo ghrepo.Interface, id string) (*domain.Autolink, error) + View(repo ghrepo.Interface, id string) (*shared.Autolink, error) } func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Command { @@ -65,7 +65,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Comman }, } - cmdutil.AddJSONFlags(cmd, &opts.Exporter, domain.AutolinkFields) + cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.AutolinkFields) return cmd } diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go index 4ace02ea5a4..849a2828af5 100644 --- a/pkg/cmd/repo/autolink/view/view_test.go +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -8,7 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "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/cli/cli/v2/pkg/jsonfieldstest" @@ -97,11 +97,11 @@ func TestNewCmdView(t *testing.T) { } type stubAutoLinkViewer struct { - autolink *domain.Autolink + autolink *shared.Autolink err error } -func (g stubAutoLinkViewer) View(repo ghrepo.Interface, id string) (*domain.Autolink, error) { +func (g stubAutoLinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) { return g.autolink, g.err } @@ -126,7 +126,7 @@ func TestViewRun(t *testing.T) { ID: "1", }, stubViewer: stubAutoLinkViewer{ - autolink: &domain.Autolink{ + autolink: &shared.Autolink{ ID: 1, KeyPrefix: "TICKET-", URLTemplate: "https://example.com/TICKET?query=", @@ -152,7 +152,7 @@ func TestViewRun(t *testing.T) { }(), }, stubViewer: stubAutoLinkViewer{ - autolink: &domain.Autolink{ + autolink: &shared.Autolink{ ID: 1, KeyPrefix: "TICKET-", URLTemplate: "https://example.com/TICKET?query=", From 58d84a4088a8cff75414dc4ce6129d3dcb6224bb Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Tue, 28 Jan 2025 12:03:03 -0500 Subject: [PATCH 0274/2338] Shorten description --- pkg/cmd/repo/autolink/view/view.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go index 38a2590a275..064fc22ca83 100644 --- a/pkg/cmd/repo/autolink/view/view.go +++ b/pkg/cmd/repo/autolink/view/view.go @@ -35,12 +35,8 @@ func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "view ", Short: "View an autolink reference", - Long: heredoc.Docf(` - View an autolink reference for a repository. - - Information about autolinks is only available to repository administrators. - `), - Args: cobra.ExactArgs(1), + Long: "View 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() From 32ee4db48444c8ee0229a0e90056cf50f6128ca5 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Tue, 28 Jan 2025 13:33:50 -0500 Subject: [PATCH 0275/2338] Autolink view HTTP tests --- pkg/cmd/repo/autolink/list/http_test.go | 2 +- pkg/cmd/repo/autolink/view/http.go | 1 - pkg/cmd/repo/autolink/view/http_test.go | 86 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 pkg/cmd/repo/autolink/view/http_test.go diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index 0fa918f6116..65289c419b7 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestAutoLinkLister_List(t *testing.T) { +func TestAutolinkLister_List(t *testing.T) { tests := []struct { name string repo ghrepo.Interface diff --git a/pkg/cmd/repo/autolink/view/http.go b/pkg/cmd/repo/autolink/view/http.go index ddba23f6de7..8dd6dc12df2 100644 --- a/pkg/cmd/repo/autolink/view/http.go +++ b/pkg/cmd/repo/autolink/view/http.go @@ -39,7 +39,6 @@ func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolin err = json.NewDecoder(resp.Body).Decode(&autolink) if err != nil { - fmt.Println(err.Error()) return nil, err } diff --git a/pkg/cmd/repo/autolink/view/http_test.go b/pkg/cmd/repo/autolink/view/http_test.go new file mode 100644 index 00000000000..5475c1f8a82 --- /dev/null +++ b/pkg/cmd/repo/autolink/view/http_test.go @@ -0,0 +1,86 @@ +package view + +import ( + "fmt" + "net/http" + "testing" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAutolinkViewer_View(t *testing.T) { + repo := ghrepo.New("OWNER", "REPO") + testAutolink := shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + } + + tests := []struct { + name string + id string + stubStatus int + stubRespJSON string + + expectedAutolink *shared.Autolink + expectErr bool + expectedErrMsg string + }{ + { + name: "200 successful view", + id: "123", + stubStatus: 200, + stubRespJSON: `{ + "id": 123, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": true + }`, + expectedAutolink: &testAutolink, + }, + { + name: "404 repo or autolink not found", + id: "123", + stubStatus: 404, + 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)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + reg.Register( + httpmock.REST( + http.MethodGet, + fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id), + ), + httpmock.StatusStringResponse(tt.stubStatus, tt.stubRespJSON), + ) + defer reg.Verify(t) + + autolinkCreator := &AutolinkViewer{ + HTTPClient: &http.Client{Transport: reg}, + } + + autolink, err := autolinkCreator.View(repo, tt.id) + + if tt.expectErr { + require.EqualError(t, err, tt.expectedErrMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedAutolink, autolink) + } + }) + } +} From 8f55dbcf61dcda17653a2cb7755a2cf0097a8ac9 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Tue, 28 Jan 2025 14:07:48 -0500 Subject: [PATCH 0276/2338] Error case test for viewRun --- pkg/cmd/repo/autolink/view/view_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go index 849a2828af5..3951d0cca08 100644 --- a/pkg/cmd/repo/autolink/view/view_test.go +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -161,6 +161,16 @@ func TestViewRun(t *testing.T) { }, wantStdout: "{\"id\":1}\n", }, + { + name: "client error", + opts: &viewOptions{}, + stubViewer: stubAutoLinkViewer{ + autolink: nil, + err: testAutolinkClientViewError{}, + }, + expectedErr: testAutolinkClientViewError{}, + wantStderr: "", + }, } for _, tt := range tests { From de7c40789ef082fbf13f4a6cdaa504c95c176fe9 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 29 Jan 2025 08:22:32 +0500 Subject: [PATCH 0277/2338] Use `defer` instead --- pkg/cmd/run/list/list.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 0b291fae530..3113fdabd72 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -119,6 +119,8 @@ func listRun(opts *ListOptions) error { } opts.IO.StartProgressIndicator() + defer opts.IO.StopProgressIndicator() + if opts.WorkflowSelector != "" { // initially the workflow state is limited to 'active' states := []workflowShared.WorkflowState{workflowShared.Active} @@ -131,7 +133,6 @@ func listRun(opts *ListOptions) error { filters.WorkflowID = workflow.ID filters.WorkflowName = workflow.Name } else { - opts.IO.StopProgressIndicator() return err } } From 82ee9a2c75a0b1dcd942a2c18da41f4d45b81698 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 29 Jan 2025 14:53:09 +0500 Subject: [PATCH 0278/2338] [gh cache delete --all] Add `--succeed-on-no-caches` flag to return exit code 0 --- pkg/cmd/cache/delete/delete.go | 33 +++++++++++++++++++++++------ pkg/cmd/cache/delete/delete_test.go | 16 ++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 65a9d696af9..81138ddd0d1 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -22,8 +22,9 @@ type DeleteOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams - DeleteAll bool - Identifier string + DeleteAll bool + SucceedOnNoCaches bool + Identifier string } func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command { @@ -33,7 +34,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd := &cobra.Command{ - Use: "delete [| | --all]", + Use: "delete [ | | --all]", Short: "Delete GitHub Actions caches", Long: heredoc.Docf(` Delete GitHub Actions caches. @@ -50,8 +51,11 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co # Delete a cache by id in a specific repo $ gh cache delete 1234 --repo cli/cli - # Delete all caches + # Delete all caches (exit code 1 on no caches) $ gh cache delete --all + + # Delete all caches (exit code 0 on no caches) + $ gh cache delete --all --succeed-on-no-caches `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -65,6 +69,10 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co return err } + if opts.SucceedOnNoCaches && (!opts.DeleteAll || len(args) == 1) { + return cmdutil.FlagErrorf("--succeed-on-no-caches must be used in conjunction with --all") + } + if !opts.DeleteAll && len(args) == 0 { return cmdutil.FlagErrorf("must provide either cache id, cache key, or use --all") } @@ -82,6 +90,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches") + cmd.Flags().BoolVarP(&opts.SucceedOnNoCaches, "succeed-on-no-caches", "s", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`") return cmd } @@ -100,12 +109,24 @@ func deleteRun(opts *DeleteOptions) error { var toDelete []string if opts.DeleteAll { + opts.IO.StartProgressIndicator() caches, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: -1}) + opts.IO.StopProgressIndicator() if err != nil { - return err + if opts.SucceedOnNoCaches { + fmt.Println(err) + return nil + } else { + return err + } } if len(caches.ActionsCaches) == 0 { - return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon()) + if opts.SucceedOnNoCaches { + fmt.Printf("%s No caches to delete\n", opts.IO.ColorScheme().SuccessIcon()) + return nil + } else { + return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon()) + } } for _, cache := range caches.ActionsCaches { toDelete = append(toDelete, strconv.Itoa(cache.Id)) diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go index 43fc7d21301..e81fdbcec97 100644 --- a/pkg/cmd/cache/delete/delete_test.go +++ b/pkg/cmd/cache/delete/delete_test.go @@ -43,6 +43,21 @@ func TestNewCmdDelete(t *testing.T) { cli: "--all", wants: DeleteOptions{DeleteAll: true}, }, + { + name: "delete all and succeed-on-no-caches flags", + cli: "--all --succeed-on-no-caches", + wants: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, + }, + { + name: "succeed-on-no-caches flag", + cli: "--succeed-on-no-caches", + wantsErr: "--succeed-on-no-caches must be used in conjunction with --all", + }, + { + name: "succeed-on-no-caches flag and id argument", + cli: "--succeed-on-no-caches 123", + wantsErr: "--succeed-on-no-caches must be used in conjunction with --all", + }, { name: "id argument and delete all flag", cli: "1 --all", @@ -72,6 +87,7 @@ func TestNewCmdDelete(t *testing.T) { } assert.NoError(t, err) assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll) + assert.Equal(t, tt.wants.SucceedOnNoCaches, gotOpts.SucceedOnNoCaches) assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier) }) } From c7dd9edd57c6183fa89510cdafc738fd497acdca Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 29 Jan 2025 16:39:56 +0500 Subject: [PATCH 0279/2338] [gh workflow run] Improve error handling for `--ref` flag --- pkg/cmd/workflow/run/run.go | 14 +++++++++----- pkg/cmd/workflow/shared/shared.go | 5 ++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index b7ab530985e..3ba64ad4a3d 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -262,11 +262,15 @@ func runRun(opts *RunOptions) error { ref := opts.Ref - if ref == "" { - ref, err = api.RepoDefaultBranch(client, repo) - if err != nil { - return fmt.Errorf("unable to determine default branch for %s: %w", ghrepo.FullName(repo), err) - } + defaultBranch, err := api.RepoDefaultBranch(client, repo) + if err != nil { + return fmt.Errorf("unable to determine default branch for %s: %w", ghrepo.FullName(repo), err) + } + + if ref != "" && ref != defaultBranch { + return fmt.Errorf("ref %s is not the default branch, and workflows must be on the default branch to be triggered by workflow_dispatch", ref) + } else { + ref = defaultBranch } states := []shared.WorkflowState{shared.Active} diff --git a/pkg/cmd/workflow/shared/shared.go b/pkg/cmd/workflow/shared/shared.go index db6b3ab122d..61511ed7d7d 100644 --- a/pkg/cmd/workflow/shared/shared.go +++ b/pkg/cmd/workflow/shared/shared.go @@ -131,7 +131,10 @@ func FindWorkflow(client *api.Client, repo ghrepo.Interface, workflowSelector st if _, err := strconv.Atoi(workflowSelector); err == nil || isWorkflowFile(workflowSelector) { workflow, err := getWorkflowByID(client, repo, workflowSelector) if err != nil { - return nil, err + var httpErr api.HTTPError + if errors.As(err, &httpErr) && httpErr.StatusCode == 404 { + return nil, fmt.Errorf("workflow %s not found on the default branch", workflowSelector) + } } return []Workflow{*workflow}, nil } From 8bab4b47535560f4bf86fa56eab0bd5fdfb97a60 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 29 Jan 2025 18:30:43 +0500 Subject: [PATCH 0280/2338] [gh project item-list] Add `iterationId` field in ProjectV2ItemFieldIterationValue --- pkg/cmd/project/shared/queries/queries.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index 080a4309726..5855e906ecc 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -235,10 +235,11 @@ type FieldValueNodes struct { Field ProjectField } `graphql:"... on ProjectV2ItemFieldDateValue"` ProjectV2ItemFieldIterationValue struct { - Title string - StartDate string - Duration int - Field ProjectField + Title string + StartDate string + Duration int + Field ProjectField + IterationId string } `graphql:"... on ProjectV2ItemFieldIterationValue"` ProjectV2ItemFieldLabelValue struct { Labels struct { @@ -1488,9 +1489,10 @@ func projectFieldValueData(v FieldValueNodes) interface{} { return v.ProjectV2ItemFieldDateValue.Date case "ProjectV2ItemFieldIterationValue": return map[string]interface{}{ - "title": v.ProjectV2ItemFieldIterationValue.Title, - "startDate": v.ProjectV2ItemFieldIterationValue.StartDate, - "duration": v.ProjectV2ItemFieldIterationValue.Duration, + "title": v.ProjectV2ItemFieldIterationValue.Title, + "startDate": v.ProjectV2ItemFieldIterationValue.StartDate, + "duration": v.ProjectV2ItemFieldIterationValue.Duration, + "iterationId": v.ProjectV2ItemFieldIterationValue.IterationId, } case "ProjectV2ItemFieldNumberValue": return v.ProjectV2ItemFieldNumberValue.Number From 8b7d6d1b3f6a36f00b46af2bd53b83783a735be0 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Wed, 29 Jan 2025 09:34:29 -0500 Subject: [PATCH 0281/2338] Remove unused test field --- pkg/cmd/repo/autolink/view/view_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go index 3951d0cca08..f66901f59f8 100644 --- a/pkg/cmd/repo/autolink/view/view_test.go +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -118,7 +118,6 @@ func TestViewRun(t *testing.T) { stubViewer stubAutoLinkViewer expectedErr error wantStdout string - wantStderr string }{ { name: "view", @@ -169,7 +168,6 @@ func TestViewRun(t *testing.T) { err: testAutolinkClientViewError{}, }, expectedErr: testAutolinkClientViewError{}, - wantStderr: "", }, } @@ -194,10 +192,6 @@ func TestViewRun(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.wantStdout, stdout.String()) } - - if tt.wantStderr != "" { - assert.Equal(t, tt.wantStderr, stderr.String()) - } }) } } From 21a5267be4405bf059a359a434d31ecb3a3d66a0 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Wed, 29 Jan 2025 09:35:05 -0500 Subject: [PATCH 0282/2338] Remove unused test field --- pkg/cmd/repo/autolink/view/view_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go index f66901f59f8..bc0a1bf7f32 100644 --- a/pkg/cmd/repo/autolink/view/view_test.go +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -173,7 +173,7 @@ func TestViewRun(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ios, _, stdout, stderr := iostreams.Test() + ios, _, stdout, _ := iostreams.Test() opts := tt.opts opts.IO = ios @@ -187,7 +187,7 @@ func TestViewRun(t *testing.T) { if tt.expectedErr != nil { require.Error(t, err) - require.ErrorIs(t, err, tt.expectedErr) + assert.ErrorIs(t, err, tt.expectedErr) } else { require.NoError(t, err) assert.Equal(t, tt.wantStdout, stdout.String()) From af538b277d8be77f6cf8e9de0c74469a26f82a1d Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 29 Jan 2025 10:10:42 -0500 Subject: [PATCH 0283/2338] Expand test case - add autolink view case for numeric - simplifying local variable usage --- pkg/cmd/repo/autolink/view/http_test.go | 32 ++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/repo/autolink/view/http_test.go b/pkg/cmd/repo/autolink/view/http_test.go index 5475c1f8a82..2402fb2d79b 100644 --- a/pkg/cmd/repo/autolink/view/http_test.go +++ b/pkg/cmd/repo/autolink/view/http_test.go @@ -14,12 +14,6 @@ import ( func TestAutolinkViewer_View(t *testing.T) { repo := ghrepo.New("OWNER", "REPO") - testAutolink := shared.Autolink{ - ID: 123, - KeyPrefix: "TICKET-", - URLTemplate: "https://example.com/TICKET?query=", - IsAlphanumeric: true, - } tests := []struct { name string @@ -32,7 +26,7 @@ func TestAutolinkViewer_View(t *testing.T) { expectedErrMsg string }{ { - name: "200 successful view", + name: "200 successful alphanumeric view", id: "123", stubStatus: 200, stubRespJSON: `{ @@ -41,7 +35,29 @@ func TestAutolinkViewer_View(t *testing.T) { "url_template": "https://example.com/TICKET?query=", "is_alphanumeric": true }`, - expectedAutolink: &testAutolink, + expectedAutolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + { + name: "200 successful numeric view", + id: "124", + stubStatus: 200, + stubRespJSON: `{ + "id": 124, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": false + }`, + expectedAutolink: &shared.Autolink{ + ID: 124, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: false, + }, }, { name: "404 repo or autolink not found", From fe21e10ae8b42315575fe03f24ec967f4c65fab3 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 29 Jan 2025 10:16:02 -0500 Subject: [PATCH 0284/2338] Keeping autolink IDs the same in all test cases --- pkg/cmd/repo/autolink/view/http_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/repo/autolink/view/http_test.go b/pkg/cmd/repo/autolink/view/http_test.go index 2402fb2d79b..5bfe9369fc6 100644 --- a/pkg/cmd/repo/autolink/view/http_test.go +++ b/pkg/cmd/repo/autolink/view/http_test.go @@ -44,16 +44,16 @@ func TestAutolinkViewer_View(t *testing.T) { }, { name: "200 successful numeric view", - id: "124", + id: "123", stubStatus: 200, stubRespJSON: `{ - "id": 124, + "id": 123, "key_prefix": "TICKET-", "url_template": "https://example.com/TICKET?query=", "is_alphanumeric": false }`, expectedAutolink: &shared.Autolink{ - ID: 124, + ID: 123, KeyPrefix: "TICKET-", URLTemplate: "https://example.com/TICKET?query=", IsAlphanumeric: false, From 077f064f4a360600061db0f2a93553641a4ce053 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Wed, 29 Jan 2025 10:21:11 -0500 Subject: [PATCH 0285/2338] Remove v1 project 'add to board' automation from prauto workflow --- .github/workflows/prauto.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml index 2596fa76c0e..67602b2ee61 100644 --- a/.github/workflows/prauto.yml +++ b/.github/workflows/prauto.yml @@ -43,26 +43,12 @@ jobs: -q ".data.repository.project.columns.nodes[] | select(.name | startswith(\"$1\")) | .id" } - addToBoard () { - gh api graphql --silent -f query=' - mutation($colID:ID!, $prID:ID!) { addProjectCard(input: { projectColumnId: $colID, contentId: $prID }) { clientMutationId } } - ' -f colID="$(colID "Needs review")" -f prID="$PRID" - } - if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null then if [ "$PR_AUTHOR_TYPE" != "Bot" ] then gh pr edit $PRNUM --add-assignee $PRAUTHOR fi - if ! errtext="$(addToBoard 2>&1)" - then - cat <<<"$errtext" >&2 - if ! grep -iq 'project already has the associated issue' <<<"$errtext" - then - exit 1 - fi - fi exit 0 fi @@ -86,5 +72,4 @@ jobs: commentPR "Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message." fi - addToBoard exit 0 From e4d720ba228d8c4221049ca30e14b05a4dc30c17 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Wed, 29 Jan 2025 11:12:28 -0500 Subject: [PATCH 0286/2338] Remove redundant error check --- pkg/cmd/repo/autolink/view/view.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go index 064fc22ca83..f892c0724f7 100644 --- a/pkg/cmd/repo/autolink/view/view.go +++ b/pkg/cmd/repo/autolink/view/view.go @@ -38,19 +38,13 @@ func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Comman Long: "View 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.BaseRepo = f.BaseRepo opts.ID = args[0] - - if err != nil { - return err - } - opts.AutolinkClient = &AutolinkViewer{HTTPClient: httpClient} if runF != nil { From e0f624ba240546f980f1190a93dd0264a84fa2c8 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Wed, 29 Jan 2025 11:41:23 -0800 Subject: [PATCH 0287/2338] Rename PRRefs to PullRequestRefs and PR comment cleanup --- git/client.go | 2 +- git/client_test.go | 2 +- pkg/cmd/pr/create/create.go | 2 +- pkg/cmd/pr/shared/finder.go | 12 ++++---- pkg/cmd/pr/shared/finder_test.go | 50 ++++++++++++++++---------------- pkg/cmd/pr/status/status.go | 2 +- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/git/client.go b/git/client.go index 30cb4306d2e..11a2e2e20a6 100644 --- a/git/client.go +++ b/git/client.go @@ -474,7 +474,7 @@ func (c *Client) RemotePushDefault(ctx context.Context) (string, error) { } // ParsePushRevision gets the value of the @{push} revision syntax -// An error here doesn't necessarily mean something are broke, but may mean that the @{push} +// An error here doesn't necessarily mean something is broken, but may mean that the @{push} // revision syntax couldn't be resolved, such as in non-centralized workflows with // push.default = simple. Downstream consumers should consider how to handle this error. func (c *Client) ParsePushRevision(ctx context.Context, branch string) (string, error) { diff --git a/git/client_test.go b/git/client_test.go index 872c8595b8a..9fa076199e5 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -769,7 +769,7 @@ func TestClientReadBranchConfig(t *testing.T) { branch.trunk.merge refs/heads/trunk branch.trunk.pushremote origin branch.trunk.gh-merge-base gh-merge-base - `), + `), }, }, branch: "trunk", diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 6c9d2a2aff3..b2abe093845 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,7 +518,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } -// TODO: Replace with the finder's PRRefs struct +// TODO: Replace with the finder's PullRequestRefs struct // trackingRef represents a ref for a remote tracking branch. type trackingRef struct { remoteName string diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 05ec24c9ad0..e4f89502c7b 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -103,7 +103,7 @@ type FindOptions struct { // // A ref is described as "remoteName/branchName", so // baseRepoName/baseBranchName -----PR-----> headRepoName/headBranchName -type PRRefs struct { +type PullRequestRefs struct { BranchName string HeadRepo ghrepo.Interface BaseRepo ghrepo.Interface @@ -112,7 +112,7 @@ type PRRefs struct { // GetPRHeadLabel returns the string that the GitHub API uses to identify the PR. This is // either just the branch name or, if the PR is originating from a fork, the fork owner // and the branch name, like :. -func (s *PRRefs) GetPRHeadLabel() string { +func (s *PullRequestRefs) GetPRHeadLabel() string { if ghrepo.IsSame(s.HeadRepo, s.BaseRepo) { return s.BranchName } @@ -227,7 +227,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } - // Suppressing these errors as we have other means of computing the PRRefs when these fail. + // Suppressing these errors as we have other means of computing the PullRequestRefs when these fail. parsedPushRevision, _ := f.parsePushRevision(f.branchName) remotePushDefault, err := f.remotePushDefault() @@ -302,8 +302,8 @@ func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) { return repo, prNumber, nil } -func ParsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, remotePushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PRRefs, error) { - prRefs := PRRefs{ +func ParsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, remotePushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PullRequestRefs, error) { + prRefs := PullRequestRefs{ BaseRepo: baseRefRepo, } @@ -323,7 +323,7 @@ func ParsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsed for i, r := range rems { remoteNames[i] = r.Name } - return PRRefs{}, fmt.Errorf("no remote for %q found in %q", parsedPushRevision, strings.Join(remoteNames, ", ")) + return PullRequestRefs{}, fmt.Errorf("no remote for %q found in %q", parsedPushRevision, strings.Join(remoteNames, ", ")) } // We assume the PR's branch name is the same as whatever f.BranchFn() returned earlier diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index a54412db51b..694e0c20d24 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -676,15 +676,15 @@ func TestParsePRRefs(t *testing.T) { currentBranchName string baseRefRepo ghrepo.Interface rems context.Remotes - wantPRRefs PRRefs + wantPRRefs PullRequestRefs wantErr error }{ { - name: "When the branch is called 'blueberries' with an empty branch config, it returns the correct PRRefs", + name: "When the branch is called 'blueberries' with an empty branch config, it returns the correct PullRequestRefs", branchConfig: git.BranchConfig{}, currentBranchName: "blueberries", baseRefRepo: remoteOrigin.Repo, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteOrigin.Repo, BaseRepo: remoteOrigin.Repo, @@ -692,11 +692,11 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the branch is called 'otherBranch' with an empty branch config, it returns the correct PRRefs", + name: "When the branch is called 'otherBranch' with an empty branch config, it returns the correct PullRequestRefs", branchConfig: git.BranchConfig{}, currentBranchName: "otherBranch", baseRefRepo: remoteOrigin.Repo, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "otherBranch", HeadRepo: remoteOrigin.Repo, BaseRepo: remoteOrigin.Repo, @@ -711,7 +711,7 @@ func TestParsePRRefs(t *testing.T) { rems: context.Remotes{ &remoteOrigin, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "pushBranch", HeadRepo: remoteOrigin.Repo, BaseRepo: remoteOrigin.Repo, @@ -727,7 +727,7 @@ func TestParsePRRefs(t *testing.T) { &remoteUpstream, &remoteOther, }, - wantPRRefs: PRRefs{}, + wantPRRefs: PullRequestRefs{}, wantErr: fmt.Errorf("no remote for %q found in %q", "origin/differentPushBranch", "upstream, other"), }, { @@ -738,7 +738,7 @@ func TestParsePRRefs(t *testing.T) { rems: context.Remotes{ &remoteOther, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "pushBranch", HeadRepo: remoteOther.Repo, BaseRepo: remoteOrigin.Repo, @@ -746,7 +746,7 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the push remote is the same as the baseRepo, it returns the baseRepo as the PRRefs HeadRepo", + name: "When the push remote is the same as the baseRepo, it returns the baseRepo as the PullRequestRefs HeadRepo", branchConfig: git.BranchConfig{ PushRemoteName: remoteOrigin.Remote.Name, }, @@ -756,7 +756,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteOrigin.Repo, BaseRepo: remoteOrigin.Repo, @@ -764,7 +764,7 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the push remote is different from the baseRepo, it returns the push remote repo as the PRRefs HeadRepo", + name: "When the push remote is different from the baseRepo, it returns the push remote repo as the PullRequestRefs HeadRepo", branchConfig: git.BranchConfig{ PushRemoteName: remoteOrigin.Remote.Name, }, @@ -774,7 +774,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteOrigin.Repo, BaseRepo: remoteUpstream.Repo, @@ -782,7 +782,7 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the push remote defined by a URL and the baseRepo is different from the push remote, it returns the push remote repo as the PRRefs HeadRepo", + name: "When the push remote defined by a URL and the baseRepo is different from the push remote, it returns the push remote repo as the PullRequestRefs HeadRepo", branchConfig: git.BranchConfig{ PushRemoteURL: remoteOrigin.Remote.FetchURL, }, @@ -792,7 +792,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteOrigin.Repo, BaseRepo: remoteUpstream.Repo, @@ -812,7 +812,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blue-upstream-berries", HeadRepo: remoteUpstream.Repo, BaseRepo: remoteOrigin.Repo, @@ -832,7 +832,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blue-upstream-berries", HeadRepo: remoteUpstream.Repo, BaseRepo: remoteOrigin.Repo, @@ -840,7 +840,7 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When remote.pushDefault is set, it returns the correct PRRefs", + name: "When remote.pushDefault is set, it returns the correct PullRequestRefs", branchConfig: git.BranchConfig{}, remotePushDefault: remoteUpstream.Remote.Name, currentBranchName: "blueberries", @@ -849,7 +849,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteUpstream.Repo, BaseRepo: remoteOrigin.Repo, @@ -857,7 +857,7 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the remote name is set on the branch, it returns the correct PRRefs", + name: "When the remote name is set on the branch, it returns the correct PullRequestRefs", branchConfig: git.BranchConfig{ RemoteName: remoteUpstream.Remote.Name, }, @@ -867,7 +867,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteUpstream.Repo, BaseRepo: remoteOrigin.Repo, @@ -875,7 +875,7 @@ func TestParsePRRefs(t *testing.T) { wantErr: nil, }, { - name: "When the remote URL is set on the branch, it returns the correct PRRefs", + name: "When the remote URL is set on the branch, it returns the correct PullRequestRefs", branchConfig: git.BranchConfig{ RemoteURL: remoteUpstream.Remote.FetchURL, }, @@ -885,7 +885,7 @@ func TestParsePRRefs(t *testing.T) { &remoteOrigin, &remoteUpstream, }, - wantPRRefs: PRRefs{ + wantPRRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: remoteUpstream.Repo, BaseRepo: remoteOrigin.Repo, @@ -911,12 +911,12 @@ func TestPRRefs_GetPRHeadLabel(t *testing.T) { upstreamRepo := ghrepo.New("UPSTREAMOWNER", "REPO") tests := []struct { name string - prRefs PRRefs + prRefs PullRequestRefs want string }{ { name: "When the HeadRepo and BaseRepo match, it returns the branch name", - prRefs: PRRefs{ + prRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: originRepo, BaseRepo: originRepo, @@ -925,7 +925,7 @@ func TestPRRefs_GetPRHeadLabel(t *testing.T) { }, { name: "When the HeadRepo and BaseRepo do not match, it returns the prepended HeadRepo owner to the branch name", - prRefs: PRRefs{ + prRefs: PullRequestRefs{ BranchName: "blueberries", HeadRepo: originRepo, BaseRepo: upstreamRepo, diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 996a178e1c7..3877fb1cf5f 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -107,7 +107,7 @@ func statusRun(opts *StatusOptions) error { if err != nil { return err } - // Suppressing these errors as we have other means of computing the PRRefs when these fail. + // Suppressing these errors as we have other means of computing the PullRequestRefs when these fail. parsedPushRevision, _ := opts.GitClient.ParsePushRevision(ctx, currentBranchName) remotePushDefault, err := opts.GitClient.RemotePushDefault(ctx) From b23cf6e8d768d9a929950c8c33eff780af79272b Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Wed, 29 Jan 2025 16:24:47 -0500 Subject: [PATCH 0288/2338] Update formatting for autolink output --- pkg/cmd/repo/autolink/create/create.go | 4 ++-- pkg/cmd/repo/autolink/list/list.go | 4 +++- pkg/cmd/repo/autolink/view/view.go | 31 +++++++++++++------------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index ac3035c79e9..aad71a9414f 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -109,9 +109,9 @@ func createRun(opts *createOptions) error { cs := opts.IO.ColorScheme() fmt.Fprintf(opts.IO.Out, - "%s Created repository autolink %d on %s\n", + "%s Created repository autolink %s on %s\n", cs.SuccessIconWithColor(cs.Green), - autolink.ID, + cs.Cyanf("%d", autolink.ID), ghrepo.FullName(repo)) return nil diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 123c5321e02..402479c44cb 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -104,8 +104,10 @@ func listRun(opts *listOptions) error { tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC")) + cs := opts.IO.ColorScheme() + for _, autolink := range autolinks { - tp.AddField(fmt.Sprintf("%d", autolink.ID)) + tp.AddField(cs.Cyanf("%d", autolink.ID)) tp.AddField(autolink.KeyPrefix) tp.AddField(autolink.URLTemplate) tp.AddField(strconv.FormatBool(autolink.IsAlphanumeric)) diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go index f892c0724f7..0b212aece9e 100644 --- a/pkg/cmd/repo/autolink/view/view.go +++ b/pkg/cmd/repo/autolink/view/view.go @@ -3,7 +3,6 @@ package view import ( "fmt" - "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" @@ -65,6 +64,8 @@ func viewRun(opts *viewOptions) error { if err != nil { return err } + + out := opts.IO.Out cs := opts.IO.ColorScheme() autolink, err := opts.AutolinkClient.View(repo, opts.ID) @@ -77,21 +78,19 @@ func viewRun(opts *viewOptions) error { return opts.Exporter.Write(opts.IO, autolink) } - msg := heredoc.Docf(` - Autolink in %s - - ID: %d - Key Prefix: %s - URL Template: %s - Alphanumeric: %t - `, - ghrepo.FullName(repo), - autolink.ID, - autolink.KeyPrefix, - autolink.URLTemplate, - autolink.IsAlphanumeric, - ) - fmt.Fprint(opts.IO.Out, msg) + fmt.Fprintf(out, "Autolink in %s\n\n", ghrepo.FullName(repo)) + + fmt.Fprint(out, cs.Bold("ID: ")) + fmt.Fprintln(out, cs.Cyanf("%d", autolink.ID)) + + fmt.Fprint(out, cs.Bold("Key Prefix: ")) + fmt.Fprintln(out, autolink.KeyPrefix) + + fmt.Fprint(out, cs.Bold("URL Template: ")) + fmt.Fprintln(out, autolink.URLTemplate) + + fmt.Fprint(out, cs.Bold("Alphanumeric: ")) + fmt.Fprintln(out, autolink.IsAlphanumeric) return nil } From 9e14550f11bb6408e95a6f96535770d03dc539ad Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 30 Jan 2025 10:21:18 +0500 Subject: [PATCH 0289/2338] [gh api] Fix mutual exclusion messages of `--slurp` flag --- pkg/cmd/api/api.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index c8eac91152f..a6eb6971887 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -239,17 +239,19 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command return err } - if opts.Slurp && !opts.Paginate { - return cmdutil.FlagErrorf("`--paginate` required when passing `--slurp`") - } + if opts.Slurp { + if err := cmdutil.MutuallyExclusive( + "the `--slurp` option is not supported with `--jq` or `--template`", + opts.Slurp, + opts.FilterOutput != "", + opts.Template != "", + ); err != nil { + return err + } - if err := cmdutil.MutuallyExclusive( - "the `--slurp` option is not supported with `--jq` or `--template`", - opts.Slurp, - opts.FilterOutput != "", - opts.Template != "", - ); err != nil { - return err + if !opts.Paginate { + return cmdutil.FlagErrorf("`--paginate` required when passing `--slurp`") + } } if err := cmdutil.MutuallyExclusive( From 3bc7b7e7e25b75fd5d6bf9b1e3b667a21c003147 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 30 Jan 2025 11:40:42 +0500 Subject: [PATCH 0290/2338] [gh extensions install] Improve help text and error message --- pkg/cmd/extension/command.go | 10 +++++++++- pkg/cmd/extension/manager.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index dad4021b643..ae9a917a6cf 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -298,9 +298,17 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { Long: heredoc.Docf(` Install a GitHub CLI extension from a GitHub or local repository. - For GitHub repositories, the repository argument can be specified in %[1]sOWNER/REPO%[1]s format or as a full repository URL. + For GitHub repositories, the repository argument can be specified in + %[1]sOWNER/REPO%[1]s format or as a full repository URL. The URL format is useful when the repository is not hosted on github.com. + For remote repositories, the GitHub CLI first looks for the release artifacts assuming + that it's a binary extension i.e. prebuilt binaries provided as part of the release. + In the absence of a release, the repository itself is cloned assuming that it's a + script extension i.e. prebuilt executable or script exists on its root. + The %[1]s--pin%[1]s flag may be used to specify a tag or commit for binary and script + extensions respectively, the latest one is used otherwise. + For local repositories, often used while developing extensions, use %[1]s.%[1]s as the value of the repository argument. Note the following: diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index c6947f47903..e83a3ed22d0 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -268,7 +268,7 @@ func (m *Manager) Install(repo ghrepo.Interface, target string) error { return err } if !hs { - return errors.New("extension is not installable: missing executable") + return fmt.Errorf("extension is not installable: no acceptable release artifact or script found in %s", repo) } return m.installGit(repo, target) From e1d1f2152557e007d1c27890d8430996c0eb7eeb Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 30 Jan 2025 12:38:50 +0500 Subject: [PATCH 0291/2338] [gh repo view] Improve error message for forked repo --- context/context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/context.go b/context/context.go index 06ef8ca7db2..e3b84e6b195 100644 --- a/context/context.go +++ b/context/context.go @@ -105,7 +105,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e fmt.Fprintln(io.Out) return nil, errors.New( - "please run `gh repo set-default` to select a default remote repository.") + "please run `gh repo set-default` to select a default remote repository.\nTo learn more about the default repository, run: gh repo set-default --help") } func (r *ResolvedRemotes) HeadRepos() ([]*api.Repository, error) { From 978d3fce87d2178e64b9cc9f9d363dc474ffcb45 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 30 Jan 2025 13:05:18 +0500 Subject: [PATCH 0292/2338] [gh issue/pr list] Improve help text --- pkg/cmd/issue/list/list.go | 3 ++- pkg/cmd/pr/list/list.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index 47da51ae415..46c0e2cb045 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -59,7 +59,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Use: "list", Short: "List issues in a repository", Long: heredoc.Doc(` - List issues in a GitHub repository. + List issues in a GitHub repository. By default, this only lists open issues. The search query syntax is documented here: @@ -70,6 +70,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman $ gh issue list --assignee "@me" $ gh issue list --milestone "The big 1.0" $ gh issue list --search "error no:assignee sort:created-asc" + $ gh issue list --state all `), Aliases: []string{"ls"}, Args: cmdutil.NoArgsQuoteReminder, diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 93a9b8b4b68..bf0c2f8ed00 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -55,7 +55,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Use: "list", Short: "List pull requests in a repository", Long: heredoc.Doc(` - List pull requests in a GitHub repository. + List pull requests in a GitHub repository. By default, this only lists open PRs. The search query syntax is documented here: From 029a5a577b2ab4ac180e4ff93ea6b74ebc09d75b Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 30 Jan 2025 19:23:07 +0500 Subject: [PATCH 0293/2338] Update test --- pkg/cmd/project/shared/queries/export_data_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/project/shared/queries/export_data_test.go b/pkg/cmd/project/shared/queries/export_data_test.go index adfdce63f56..c9f991ed44e 100644 --- a/pkg/cmd/project/shared/queries/export_data_test.go +++ b/pkg/cmd/project/shared/queries/export_data_test.go @@ -321,7 +321,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing. assert.NoError(t, err) assert.JSONEq( t, - `{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, + `{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0,"iterationId":""},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, string(out)) } From 015c73005a89807111dfb0212b539818e59225cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:25:54 +0000 Subject: [PATCH 0294/2338] Bump github.com/spf13/pflag from 1.0.5 to 1.0.6 Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.5 to 1.0.6. - [Release notes](https://github.com/spf13/pflag/releases) - [Commits](https://github.com/spf13/pflag/compare/v1.0.5...v1.0.6) --- updated-dependencies: - dependency-name: github.com/spf13/pflag dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 38837b88320..3e843973ad8 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/sigstore/protobuf-specs v0.3.3 github.com/sigstore/sigstore-go v0.7.0 github.com/spf13/cobra v1.8.1 - github.com/spf13/pflag v1.0.5 + github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/zalando/go-keyring v0.2.5 golang.org/x/crypto v0.32.0 diff --git a/go.sum b/go.sum index 883ac72cefc..725a9d7559c 100644 --- a/go.sum +++ b/go.sum @@ -433,8 +433,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 313faf9cd08d8aa47cb2b8118410852393b7597e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 07:43:13 -0700 Subject: [PATCH 0295/2338] add signer and source ref, commit options Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy.go | 44 +++++++---------- pkg/cmd/attestation/verify/policy_test.go | 57 +++++++++++++++++++++++ pkg/cmd/attestation/verify/verify.go | 10 ++-- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index bb6ddb27863..759867509b3 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -13,8 +13,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/verification" ) -const hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$` -const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/[a-zA-Z0-9-]+.(yml|yaml)$` +const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/.*\/[a-zA-Z0-9-]+.(yml|yaml)$` func expandToGitHubURL(tenant, ownerOrRepo string) string { if tenant == "" { @@ -57,11 +56,17 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er signedRepoRegex := expandToGitHubURLRegex(opts.Tenant, opts.SignerRepo) c.SANRegex = signedRepoRegex } else if opts.SignerWorkflow != "" { - validatedWorkflowRegex, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow) + validatedWorkflow, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow) if err != nil { return verification.EnforcementCriteria{}, err } - c.SANRegex = validatedWorkflowRegex + + workflowRegex := fmt.Sprintf("^%s", validatedWorkflow) + c.SANRegex = workflowRegex + + if opts.SignerRef != "" { + c.Certificate.BuildSignerURI = fmt.Sprintf("%s@%s", validatedWorkflow, opts.SignerRef) + } } else if opts.Repo != "" { // if the user has not provided the SAN, SANRegex, SignerRepo, or SignerWorkflow options // then we default to the repo option @@ -99,26 +104,11 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er c.Certificate.Issuer = opts.OIDCIssuer } - if opts.SignerDigest != "" { - c.Certificate.BuildSignerDigest = opts.SignerDigest - } - - if opts.SignerRef != "" { - // need to build the full URI value - uri, err := getFullWorkflowURI(c.SANRegex) - if err != nil { - return verification.EnforcementCriteria{}, err - } - c.Certificate.BuildSignerURI = uri - } - - if opts.SourceDigest != "" { - c.Certificate.SourceRepositoryDigest = opts.SourceDigest - } - - if opts.SourceRef != "" { - c.Certificate.SourceRepositoryRef = opts.SourceRef - } + // set the SourceRepositoryDigest, SourceRepositoryRef, and BuildSignerDigest + // extensions if the options are provided + c.Certificate.BuildSignerDigest = opts.SignerDigest + c.Certificate.SourceRepositoryDigest = opts.SourceDigest + c.Certificate.SourceRepositoryRef = opts.SourceRef return c, nil } @@ -179,13 +169,13 @@ func buildSigstoreVerifyPolicy(c verification.EnforcementCriteria, a artifact.Di func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) { // we expect a provided workflow argument be in the format [HOST/]///path/to/workflow.yml // if the provided workflow does not contain a host, set the host - match, err := regexp.MatchString(hostRegex, signerWorkflow) + match, err := regexp.MatchString(workflowURIRegex, signerWorkflow) if err != nil { return "", err } if match { - return fmt.Sprintf("^https://%s", signerWorkflow), nil + return fmt.Sprintf("https://%s", signerWorkflow), nil } // if the provided workflow did not match the expect format @@ -194,5 +184,5 @@ func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) { return "", errors.New("unknown host") } - return fmt.Sprintf("^https://%s/%s", hostname, signerWorkflow), nil + return fmt.Sprintf("https://%s/%s", hostname, signerWorkflow), nil } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index ecaf74ec361..a05e3377dba 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -217,6 +217,63 @@ func TestNewEnforcementCriteria(t *testing.T) { require.NoError(t, err) require.Equal(t, "https://foo.com", c.Certificate.Issuer) }) + + t.Run("sets Certificate.BuildSignerURI using SignerWorkflow and SignerRef", func(t *testing.T) { + opts := &Options{ + ArtifactPath: artifactPath, + Owner: "wrong", + Repo: "wrong/value", + SignerWorkflow: "foo/bar/.github/workflows/attest.yml", + SignerRef: "refs/heads/main", + Hostname: "github.com", + } + + c, err := newEnforcementCriteria(opts) + require.NoError(t, err) + require.Equal(t, "https://github.com/foo/bar/.github/workflows/attest.yml@refs/heads/main", c.Certificate.BuildSignerURI) + }) + + t.Run("sets Certificate.BuildSignerDigest using opts.SignerDigest", func(t *testing.T) { + opts := &Options{ + ArtifactPath: artifactPath, + Owner: "wrong", + Repo: "wrong/value", + SignerDigest: "foo", + Hostname: "github.com", + } + + c, err := newEnforcementCriteria(opts) + require.NoError(t, err) + require.Equal(t, "foo", c.Certificate.BuildSignerDigest) + }) + + t.Run("sets Certificate.SourceRepositoryDigest using opts.SourceDigest", func(t *testing.T) { + opts := &Options{ + ArtifactPath: artifactPath, + Owner: "wrong", + Repo: "wrong/value", + SourceDigest: "foo", + Hostname: "github.com", + } + + c, err := newEnforcementCriteria(opts) + require.NoError(t, err) + require.Equal(t, "foo", c.Certificate.SourceRepositoryDigest) + }) + + t.Run("sets Certificate.SourceRepositoryRef using opts.SourceRef", func(t *testing.T) { + opts := &Options{ + ArtifactPath: artifactPath, + Owner: "wrong", + Repo: "wrong/value", + SourceRef: "refs/heads/main", + Hostname: "github.com", + } + + c, err := newEnforcementCriteria(opts) + require.NoError(t, err) + require.Equal(t, "refs/heads/main", c.Certificate.SourceRepositoryRef) + }) } func TestValidateSignerWorkflow(t *testing.T) { diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index ea7502f004c..c1ec63bbbdf 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -187,14 +187,18 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") cmdutil.AddFormatFlags(verifyCmd, &opts.exporter) // policy enforcement flags - verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners") verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly") verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex") - verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") - verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow") verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token") + verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners") verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") + verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Digest associated with the signer workflow") + verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") + verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") + verifyCmd.Flags().StringVarP(&opts.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this") + verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Ref associated with the source workflow") + verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Digest associated with the source workflow") return verifyCmd } From c6b5928ddc6b6e92524d4890192d417daf07d5d9 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 07:58:42 -0700 Subject: [PATCH 0296/2338] fix issues causing tests to fail Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/policy.go | 15 ++++++++++++++- pkg/cmd/attestation/verify/policy.go | 18 ++---------------- pkg/cmd/attestation/verify/policy_test.go | 22 ++++------------------ pkg/cmd/attestation/verify/verify.go | 2 +- 4 files changed, 21 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go index f2bd126d096..487542a58b9 100644 --- a/pkg/cmd/attestation/verification/policy.go +++ b/pkg/cmd/attestation/verification/policy.go @@ -52,7 +52,7 @@ func (c EnforcementCriteria) Valid() error { } func (c EnforcementCriteria) BuildPolicyInformation() string { - policyAttr := make([][]string, 0, 6) + policyAttr := [][]string{} policyAttr = appendStr(policyAttr, "- Predicate type must match", c.PredicateType) @@ -62,6 +62,19 @@ func (c EnforcementCriteria) BuildPolicyInformation() string { policyAttr = appendStr(policyAttr, "- Source Repository URI must match", c.Certificate.SourceRepositoryURI) } + if c.Certificate.BuildSignerDigest != "" { + policyAttr = appendStr(policyAttr, "- Build signer digest must match", c.Certificate.BuildSignerDigest) + } + if c.Certificate.BuildSignerURI != "" { + policyAttr = appendStr(policyAttr, "- Build signer URI must match", c.Certificate.BuildSignerURI) + } + if c.Certificate.SourceRepositoryDigest != "" { + policyAttr = appendStr(policyAttr, "- Source repo digest digest must match", c.Certificate.SourceRepositoryDigest) + } + if c.Certificate.SourceRepositoryRef != "" { + policyAttr = appendStr(policyAttr, "- Source repo ref must match", c.Certificate.SourceRepositoryRef) + } + if c.SAN != "" { policyAttr = appendStr(policyAttr, "- Subject Alternative Name must match", c.SAN) } else if c.SANRegex != "" { diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 759867509b3..c42d873d64e 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -13,7 +13,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/verification" ) -const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/.*\/[a-zA-Z0-9-]+.(yml|yaml)$` +const hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$` func expandToGitHubURL(tenant, ownerOrRepo string) string { if tenant == "" { @@ -113,20 +113,6 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er return c, nil } -func getFullWorkflowURI(s string) (string, error) { - trimmed, _ := strings.CutPrefix(s, "^") - match, err := regexp.MatchString(workflowURIRegex, trimmed) - if err != nil { - return "", err - } - - if !match { - return "", nil - } - - return trimmed, nil -} - func buildCertificateIdentityOption(c verification.EnforcementCriteria) (verify.PolicyOption, error) { sanMatcher, err := verify.NewSANMatcher(c.SAN, c.SANRegex) if err != nil { @@ -169,7 +155,7 @@ func buildSigstoreVerifyPolicy(c verification.EnforcementCriteria, a artifact.Di func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) { // we expect a provided workflow argument be in the format [HOST/]///path/to/workflow.yml // if the provided workflow does not contain a host, set the host - match, err := regexp.MatchString(workflowURIRegex, signerWorkflow) + match, err := regexp.MatchString(hostRegex, signerWorkflow) if err != nil { return "", err } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index a05e3377dba..83ab893de0c 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -1,7 +1,6 @@ package verify import ( - "fmt" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" @@ -296,25 +295,25 @@ func TestValidateSignerWorkflow(t *testing.T) { { name: "workflow with default host", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "github.com", }, { name: "workflow with workflow URL included", providedSignerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "github.com", }, { name: "workflow with GH_HOST set", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "^https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "myhost.github.com", }, { name: "workflow with authenticated host", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "authedhost.github.com", }, } @@ -333,16 +332,3 @@ func TestValidateSignerWorkflow(t *testing.T) { } } } - -func TestGetFullWorkflowURI(t *testing.T) { - expectedURI := "https://github.com/foo/bar/.github/workflows/mybuildjob.yaml" - // exact matching - uri, err := getFullWorkflowURI(expectedURI) - require.NoError(t, err) - require.Equal(t, expectedURI, uri) - - // matching after stripping regex prefix characters - uri, err = getFullWorkflowURI(fmt.Sprintf("^%s", expectedURI)) - require.NoError(t, err) - require.Equal(t, expectedURI, uri) -} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index c1ec63bbbdf..97fe8584592 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -194,9 +194,9 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners") verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Digest associated with the signer workflow") + verifyCmd.Flags().StringVarP(&opts.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this") verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") - verifyCmd.Flags().StringVarP(&opts.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this") verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Ref associated with the source workflow") verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Digest associated with the source workflow") From e10010c4cfc9de2c3f36c87a4cbf8726cd01c766 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 08:03:36 -0700 Subject: [PATCH 0297/2338] fix option ordering Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 97fe8584592..f95867e8a99 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -187,16 +187,16 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") cmdutil.AddFormatFlags(verifyCmd, &opts.exporter) // policy enforcement flags + verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners") verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly") verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex") + verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") + verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow") verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token") - verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners") verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Digest associated with the signer workflow") verifyCmd.Flags().StringVarP(&opts.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this") - verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") - verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Ref associated with the source workflow") verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Digest associated with the source workflow") From 1c326c74f01c0a7b817b79739b3a0479a6a3bed4 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 08:14:13 -0700 Subject: [PATCH 0298/2338] add checks to cert extensions func Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/extensions.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 2958408d0c2..6770dca6357 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -59,5 +59,18 @@ func verifyCertExtensions(given, expected certificate.Summary) error { return fmt.Errorf("expected Issuer to be %s, got %s", expected.Issuer, given.Issuer) } + if expected.BuildSignerDigest != "" && !strings.EqualFold(expected.BuildSignerDigest, given.BuildSignerDigest) { + return fmt.Errorf("expected BuildSignerDigest to be %s, got %s", expected.BuildSignerDigest, given.BuildSignerDigest) + } + if expected.BuildSignerURI != "" && !strings.EqualFold(expected.BuildSignerURI, given.BuildSignerURI) { + return fmt.Errorf("expected BuildSignerURI to be %s, got %s", expected.BuildSignerURI, given.BuildSignerURI) + } + if expected.SourceRepositoryDigest != "" && !strings.EqualFold(expected.SourceRepositoryDigest, given.SourceRepositoryDigest) { + return fmt.Errorf("expected SourceRepositoryDigest to be %s, got %s", expected.SourceRepositoryDigest, given.SourceRepositoryDigest) + } + if expected.SourceRepositoryRef != "" && !strings.EqualFold(expected.SourceRepositoryRef, given.SourceRepositoryRef) { + return fmt.Errorf("expected SourceRepositoryRef to be %s, got %s", expected.SourceRepositoryRef, given.SourceRepositoryRef) + } + return nil } From ddb8855198ea7a5bc9547e205dbfda8b7b5f1b80 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 09:12:48 -0700 Subject: [PATCH 0299/2338] flip bundle fetching logic Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 22 +++++++++++----------- pkg/cmd/attestation/api/client_test.go | 8 ++++++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 37579b7bccf..746b0441152 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -176,23 +176,23 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ return fmt.Errorf("attestation has no bundle or bundle URL") } - // for now, we fallback to the bundle field if the bundle URL is empty - if a.BundleURL == "" { - c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") + // If the bundle field is nil, try to fetch the bundle with the provided URL + if a.Bundle == nil { + b, err := c.GetBundle(a.BundleURL) + if err != nil { + return fmt.Errorf("failed to fetch bundle with URL: %w", err) + } fetched[i] = &Attestation{ - Bundle: a.Bundle, + Bundle: b, } - return nil } - // otherwise fetch the bundle with the provided URL - b, err := c.GetBundle(a.BundleURL) - if err != nil { - return fmt.Errorf("failed to fetch bundle with URL: %w", err) - } + // otherwise fall back to the bundle field + c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") fetched[i] = &Attestation{ - Bundle: b, + Bundle: a.Bundle, } + return nil }) } diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 65f8d59ca3c..083a5c5c817 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -180,7 +180,7 @@ func TestGetByDigest_Error(t *testing.T) { require.Nil(t, attestations) } -func TestFetchBundleFromAttestations(t *testing.T) { +func TestFetchBundleFromAttestations_Fallback_Bundle_Field(t *testing.T) { httpClient := &mockHttpClient{} client := LiveClient{ httpClient: httpClient, @@ -194,7 +194,7 @@ func TestFetchBundleFromAttestations(t *testing.T) { require.NoError(t, err) require.Len(t, fetched, 2) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) - httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) + httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 0) } func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { @@ -220,7 +220,10 @@ func TestFetchBundleFromAttestations_Fail(t *testing.T) { } att1 := makeTestAttestation() + att1.Bundle = nil att2 := makeTestAttestation() + att2.Bundle = nil + // zero out the bundle field so it tries fetching by URL attestations := []*Attestation{&att1, &att2} fetched, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) @@ -237,6 +240,7 @@ func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { } a := makeTestAttestation() + a.Bundle = nil attestations := []*Attestation{&a} bundle, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) From 70ae9f39ef3668bab984a77be34b2ec3be4162b5 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 09:26:41 -0700 Subject: [PATCH 0300/2338] update tests to account for logic flip Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 4 +++- pkg/cmd/attestation/api/client_test.go | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 746b0441152..6054bc98e2f 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -178,6 +178,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ // If the bundle field is nil, try to fetch the bundle with the provided URL if a.Bundle == nil { + c.logger.VerbosePrintf("Bundle field is empty. Trying to fetch with bundle URL\n\n") b, err := c.GetBundle(a.BundleURL) if err != nil { return fmt.Errorf("failed to fetch bundle with URL: %w", err) @@ -185,10 +186,11 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ fetched[i] = &Attestation{ Bundle: b, } + return nil } // otherwise fall back to the bundle field - c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n") + c.logger.VerbosePrintf("Fetching bundle from Bundle field\n\n") fetched[i] = &Attestation{ Bundle: a.Bundle, } diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 083a5c5c817..3d180af8f41 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -180,7 +180,7 @@ func TestGetByDigest_Error(t *testing.T) { require.Nil(t, attestations) } -func TestFetchBundleFromAttestations_Fallback_Bundle_Field(t *testing.T) { +func TestFetchBundleFromAttestations_BundleURL(t *testing.T) { httpClient := &mockHttpClient{} client := LiveClient{ httpClient: httpClient, @@ -188,13 +188,16 @@ func TestFetchBundleFromAttestations_Fallback_Bundle_Field(t *testing.T) { } att1 := makeTestAttestation() + att1.Bundle = nil att2 := makeTestAttestation() + att2.Bundle = nil + // zero out the bundle field so it tries fetching by URL attestations := []*Attestation{&att1, &att2} fetched, err := client.fetchBundleFromAttestations(attestations) require.NoError(t, err) require.Len(t, fetched, 2) - require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) - httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 0) + require.NotNil(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType()) + httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) } func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { @@ -211,7 +214,7 @@ func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { require.Nil(t, fetched, 2) } -func TestFetchBundleFromAttestations_Fail(t *testing.T) { +func TestFetchBundleFromAttestations_Fail_BundleURL(t *testing.T) { httpClient := &failAfterOneCallHttpClient{} c := &LiveClient{ From e9f7761423e3ee24f1debc2621dad92af65ece38 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 09:56:06 -0700 Subject: [PATCH 0301/2338] dont retry when parsing fails Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 76ef6688ad1..a950e76eb13 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -209,19 +209,19 @@ func (c *LiveClient) getBundle(url string) (*bundle.Bundle, error) { var out []byte decompressed, err := snappy.Decode(out, body) if err != nil { - return fmt.Errorf("failed to decompress with snappy: %w", err) + return backoff.Permanent(fmt.Errorf("failed to decompress with snappy: %w", err)) } var pbBundle v1.Bundle if err = protojson.Unmarshal(decompressed, &pbBundle); err != nil { - return fmt.Errorf("failed to unmarshal to bundle: %w", err) + return backoff.Permanent(fmt.Errorf("failed to unmarshal to bundle: %w", err)) } c.logger.VerbosePrintf("Successfully fetched bundle\n\n") sgBundle, err = bundle.NewBundle(&pbBundle) if err != nil { - return fmt.Errorf("failed to create new bundle: %w", err) + return backoff.Permanent(fmt.Errorf("failed to create new bundle: %w", err)) } return nil From d1b5facb48f11d95af08b12b4697f342ce8db025 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Thu, 30 Jan 2025 12:59:29 -0500 Subject: [PATCH 0302/2338] Remove unused env var, PRID --- .github/workflows/prauto.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml index 67602b2ee61..b7ed90183de 100644 --- a/.github/workflows/prauto.yml +++ b/.github/workflows/prauto.yml @@ -16,7 +16,6 @@ jobs: env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} - PRID: ${{ github.event.pull_request.node_id }} PRBODY: ${{ github.event.pull_request.body }} PRNUM: ${{ github.event.pull_request.number }} PRHEAD: ${{ github.event.pull_request.head.label }} From df250a2b538e2860f41b2b370361f291c687407f Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Thu, 30 Jan 2025 13:07:21 -0500 Subject: [PATCH 0303/2338] Update deployment.yml Fix failing tag validate job --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 31cd4b8e7d4..efc729711cd 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Validate tag name format run: | - if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$]]; then + if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Invalid tag name format. Must be in the form v1.2.3" exit 1 fi From 795263524d1415592588761051b5d9ed97e93bc5 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 11:11:41 -0700 Subject: [PATCH 0304/2338] change permanent backoff error condition Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index a950e76eb13..1e99a2a068d 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -193,7 +193,7 @@ func (c *LiveClient) getBundle(url string) (*bundle.Bundle, error) { err := backoff.Retry(func() error { resp, err := c.httpClient.Get(url) if err != nil { - return backoff.Permanent(err) + return fmt.Errorf("request to fetch bundle from URL failed: %w", err) } if resp.StatusCode >= 500 && resp.StatusCode <= 599 { From fc2f18c896605b7274f83a45def22a121a9a7d69 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 11:12:58 -0700 Subject: [PATCH 0305/2338] consolidate tests around getBundle func when possible Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/api/client_test.go | 88 ++++++++++--------- .../attestation/api/mock_httpClient_test.go | 24 ++++- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index cb39fb78a37..787408a4e79 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -170,10 +170,7 @@ func TestFetchBundleFromAttestations_BundleURL(t *testing.T) { } att1 := makeTestAttestation() - att1.Bundle = nil att2 := makeTestAttestation() - att2.Bundle = nil - // zero out the bundle field so it tries fetching by URL attestations := []*Attestation{&att1, &att2} fetched, err := client.fetchBundleFromAttestations(attestations) require.NoError(t, err) @@ -182,58 +179,46 @@ func TestFetchBundleFromAttestations_BundleURL(t *testing.T) { httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2) } -func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { +func TestFetchBundleFromAttestations_MissingBundleAndBundleURLFields(t *testing.T) { httpClient := &mockHttpClient{} client := LiveClient{ httpClient: httpClient, logger: io.NewTestHandler(), } + // If both the BundleURL and Bundle fields are empty, the function should + // return an error indicating that att1 := Attestation{} attestations := []*Attestation{&att1} - fetched, err := client.fetchBundleFromAttestations(attestations) - require.Error(t, err) - require.Nil(t, fetched, 2) + bundles, err := client.fetchBundleFromAttestations(attestations) + require.ErrorContains(t, err, "attestation has no bundle or bundle URL") + require.Nil(t, bundles, 2) } -func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { - mockHTTPClient := &failHttpClient{} - // failAfterOneCallHttpClient - - c := &LiveClient{ - httpClient: mockHTTPClient, - logger: io.NewTestHandler(), +func TestFetchBundleFromAttestations_FailOnTheSecondAttestation(t *testing.T) { + mockHTTPClient := &failAfterNCallsHttpClient{ + // the initial HTTP request will succeed, which returns a bundle for the first attestation + // all following HTTP requests will fail, which means the function fails to fetch a bundle + // for the second attestation and the function returns an error + FailOnCallN: 2, + FailOnAllSubsequentCalls: true, } - a := makeTestAttestation() - attestations := []*Attestation{&a} - bundle, err := c.fetchBundleFromAttestations(attestations) - require.Error(t, err) - require.Nil(t, bundle) -} - -func TestFetchBundleFromAttestations_FailWithGetErr(t *testing.T) { - mockHTTPClient := &failHttpClient{} - c := &LiveClient{ httpClient: mockHTTPClient, logger: io.NewTestHandler(), } - a := makeTestAttestation() - a.Bundle = nil - attestations := []*Attestation{&a} - bundle, err := c.fetchBundleFromAttestations(attestations) + att1 := makeTestAttestation() + att2 := makeTestAttestation() + attestations := []*Attestation{&att1, &att2} + bundles, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) - require.Nil(t, bundle) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) + require.Nil(t, bundles) } -func TestFetchBundleFromAttestations_FetchByURL5XX_Resp(t *testing.T) { - mockHTTPClient := &failAfterNCallsHttpClient{ - FailOnCallN: 1, - FailOnAllSubsequentCalls: true, - } +func TestFetchBundleFromAttestations_FailAfterRetrying(t *testing.T) { + mockHTTPClient := &reqFailHttpClient{} c := &LiveClient{ httpClient: mockHTTPClient, @@ -245,6 +230,7 @@ func TestFetchBundleFromAttestations_FetchByURL5XX_Resp(t *testing.T) { bundle, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) require.Nil(t, bundle) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetReqFail", 4) } func TestFetchBundleFromAttestations_FallbackToBundleField(t *testing.T) { @@ -255,6 +241,7 @@ func TestFetchBundleFromAttestations_FallbackToBundleField(t *testing.T) { logger: io.NewTestHandler(), } + // If the bundle URL is empty, the code will fallback to the bundle field a := Attestation{Bundle: data.SigstoreBundle(t)} attestations := []*Attestation{&a} fetched, err := c.fetchBundleFromAttestations(attestations) @@ -263,6 +250,7 @@ func TestFetchBundleFromAttestations_FallbackToBundleField(t *testing.T) { mockHTTPClient.AssertNotCalled(t, "OnGetSuccess") } +// getBundle successfully fetches a bundle on the first HTTP request attempt func TestGetBundle(t *testing.T) { mockHTTPClient := &mockHttpClient{} @@ -271,13 +259,15 @@ func TestGetBundle(t *testing.T) { logger: io.NewTestHandler(), } - b, err := c.getBundle("whatever") + b, err := c.getBundle("https://mybundleurl.com") require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType()) mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1) } -func TestGetBundle_Retry(t *testing.T) { +// getBundle retries successfully when the initial HTTP request returns +// a 5XX status code +func TestGetBundle_SuccessfulRetry(t *testing.T) { mockHTTPClient := &failAfterNCallsHttpClient{ FailOnCallN: 1, FailOnAllSubsequentCalls: false, @@ -294,8 +284,26 @@ func TestGetBundle_Retry(t *testing.T) { mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2) } -func TestGetBundle_Fail(t *testing.T) { - mockHTTPClient := &failHttpClient{} +// getBundle does not retry when the function fails with a permanent backoff error condition +func TestGetBundle_PermanentBackoffFail(t *testing.T) { + mockHTTPClient := &invalidBundleClient{} + c := &LiveClient{ + httpClient: mockHTTPClient, + logger: io.NewTestHandler(), + } + + b, err := c.getBundle("mybundleurl") + // var permanent *backoff.PermanentError + //require.IsType(t, &backoff.PermanentError{}, err) + require.Error(t, err) + require.Nil(t, b) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetInvalidBundle", 1) +} + +// getBundle retries when the HTTP request fails +func TestGetBundle_RequestFail(t *testing.T) { + mockHTTPClient := &reqFailHttpClient{} + c := &LiveClient{ httpClient: mockHTTPClient, logger: io.NewTestHandler(), @@ -304,7 +312,7 @@ func TestGetBundle_Fail(t *testing.T) { b, err := c.getBundle("mybundleurl") require.Error(t, err) require.Nil(t, b) - mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1) + mockHTTPClient.AssertNumberOfCalls(t, "OnGetReqFail", 4) } func TestGetTrustDomain(t *testing.T) { diff --git a/pkg/cmd/attestation/api/mock_httpClient_test.go b/pkg/cmd/attestation/api/mock_httpClient_test.go index e11266a6073..26933ae2e86 100644 --- a/pkg/cmd/attestation/api/mock_httpClient_test.go +++ b/pkg/cmd/attestation/api/mock_httpClient_test.go @@ -27,13 +27,29 @@ func (m *mockHttpClient) Get(url string) (*http.Response, error) { }, nil } -type failHttpClient struct { +type invalidBundleClient struct { mock.Mock } -func (m *failHttpClient) Get(url string) (*http.Response, error) { - m.On("OnGetFail").Return() - m.MethodCalled("OnGetFail") +func (m *invalidBundleClient) Get(url string) (*http.Response, error) { + m.On("OnGetInvalidBundle").Return() + m.MethodCalled("OnGetInvalidBundle") + + var compressed []byte + compressed = snappy.Encode(compressed, []byte("invalid bundle bytes")) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(compressed)), + }, nil +} + +type reqFailHttpClient struct { + mock.Mock +} + +func (m *reqFailHttpClient) Get(url string) (*http.Response, error) { + m.On("OnGetReqFail").Return() + m.MethodCalled("OnGetReqFail") return &http.Response{ StatusCode: 500, From d6be4981be31da2a2365df0ffbb58163c66ce130 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 31 Jan 2025 12:28:00 +0500 Subject: [PATCH 0306/2338] Address PR review comments --- pkg/cmd/cache/delete/delete.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 81138ddd0d1..e244cc0a119 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -69,7 +69,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co return err } - if opts.SucceedOnNoCaches && (!opts.DeleteAll || len(args) == 1) { + if !opts.DeleteAll && opts.SucceedOnNoCaches { return cmdutil.FlagErrorf("--succeed-on-no-caches must be used in conjunction with --all") } @@ -113,16 +113,11 @@ func deleteRun(opts *DeleteOptions) error { caches, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: -1}) opts.IO.StopProgressIndicator() if err != nil { - if opts.SucceedOnNoCaches { - fmt.Println(err) - return nil - } else { - return err - } + return err } if len(caches.ActionsCaches) == 0 { if opts.SucceedOnNoCaches { - fmt.Printf("%s No caches to delete\n", opts.IO.ColorScheme().SuccessIcon()) + fmt.Fprintf(opts.IO.Out, "%s No caches to delete\n", opts.IO.ColorScheme().SuccessIcon()) return nil } else { return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon()) From abe6d8dd306b947eaa7421b3fa154a85cdc2ded4 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 31 Jan 2025 13:18:46 +0500 Subject: [PATCH 0307/2338] [docs/releasing.md] Add basic info for homebrew update flow --- docs/releasing.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 4b977efdda0..f1b87a9a726 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,15 +1,20 @@ # 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] +> +> Every production release will request an approval by the select few people +> before it can proceed. What this does is: + - Builds Linux binaries on Ubuntu; - Builds and signs Windows binaries on Windows; - Builds, signs, and notarizes macOS binaries on macOS; @@ -18,22 +23,31 @@ What this does is: - 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 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). + - The [GitHub CLI deployment workflow](.github/workflows/deployment.yml) + initiates a PR to update the `gh` Homebrew formula. + - This updated formula is then picked up by the scheduled workflow for its + inclusion in the `homebrew/homebrew-core`. 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 -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 +59,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 From 29e57ee8e769261303e4fced19daf9636bc3eb10 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 31 Jan 2025 13:47:21 -0800 Subject: [PATCH 0308/2338] Add tests for using the pr Finder outside of repo This includes an acceptance test for `gh pr view` and a unit test on `TestFind` --- .../testdata/pr/pr-view-outside-repo.txtar | 28 ++++++++++++++++ pkg/cmd/pr/shared/finder_test.go | 32 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 acceptance/testdata/pr/pr-view-outside-repo.txtar diff --git a/acceptance/testdata/pr/pr-view-outside-repo.txtar b/acceptance/testdata/pr/pr-view-outside-repo.txtar new file mode 100644 index 00000000000..edfb37ed4c1 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-outside-repo.txtar @@ -0,0 +1,28 @@ +# 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/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private + +# Defer repo cleanup +defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING + +# Clone the repo +exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING + +# Prepare a branch to PR +cd $SCRIPT_NAME-$RANDOM_STRING +exec git checkout -b feature-branch +exec git commit --allow-empty -m 'Empty Commit' +exec git push -u origin feature-branch + +# Create the PR +exec gh pr create --title 'Feature Title' --body 'Feature Body' +stdout2env PR_URL + +# Change directories +cd .. + +# View the PR +exec gh pr view $PR_URL +stdout 'Feature Title' diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 694e0c20d24..435377d9c43 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -211,6 +211,38 @@ func TestFind(t *testing.T) { wantPR: 13, wantRepo: "https://example.org/OWNER/REPO", }, + { + name: "PR URL argument and not in a local git repo", + args: args{ + selector: "https://example.org/OWNER/REPO/pull/13/files", + fields: []string{"id", "number"}, + baseRepoFn: nil, + branchFn: func() (string, error) { + return "", &git.GitError{ + Stderr: "fatal: not a git repository (or any of the parent directories): .git", + ExitCode: 128, + } + }, + branchConfig: stubBranchConfig(git.BranchConfig{}, &git.GitError{ + Stderr: "fatal: not a git repository (or any of the parent directories): .git", + ExitCode: 128, + }), + pushDefault: stubPushDefault("simple", nil), + remotePushDefault: stubRemotePushDefault("", &git.GitError{ + Stderr: "fatal: not a git repository (or any of the parent directories): .git", + ExitCode: 128, + }), + }, + httpStub: func(r *httpmock.Registry) { + r.Register( + httpmock.GraphQL(`query PullRequestByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ + "pullRequest":{"number":13} + }}}`)) + }, + wantPR: 13, + wantRepo: "https://example.org/OWNER/REPO", + }, { name: "when provided branch argument with an open and closed PR for that branch name, it returns the open PR", args: args{ From 058cb2c220d0200779a9742f8bba9886f3e98592 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 31 Jan 2025 13:48:08 -0800 Subject: [PATCH 0309/2338] Refactor finder to work with URL selectors --- git/command.go | 5 ++ git/command_test.go | 98 +++++++++++++++++++++++++++++++++++ git/errors.go | 1 + pkg/cmd/pr/shared/finder.go | 100 ++++++++++++++++++++---------------- 4 files changed, 160 insertions(+), 44 deletions(-) create mode 100644 git/command_test.go diff --git a/git/command.go b/git/command.go index 8065ffd86be..b68b595252c 100644 --- a/git/command.go +++ b/git/command.go @@ -6,6 +6,7 @@ import ( "errors" "io" "os/exec" + "strings" "github.com/cli/cli/v2/internal/run" ) @@ -48,6 +49,10 @@ func (gc *Command) Output() ([]byte, error) { ge.Stderr = string(exitError.Stderr) ge.ExitCode = exitError.ExitCode() } + + if strings.Contains(ge.Stderr, "fatal: not a git repository") { + ge.err = ErrNoGitRepository + } err = &ge } return out, err diff --git a/git/command_test.go b/git/command_test.go new file mode 100644 index 00000000000..15fd291fc0c --- /dev/null +++ b/git/command_test.go @@ -0,0 +1,98 @@ +package git + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOutput(t *testing.T) { + tests := []struct { + name string + exitCode int + stdout string + stderr string + wantErr error + }{ + { + name: "successful command", + stdout: "hello world", + stderr: "", + exitCode: 0, + wantErr: nil, + }, + { + name: "not a repo failure", + stdout: "", + stderr: "fatal: not a git repository (or any of the parent directories): .git", + exitCode: 128, + wantErr: ErrNoGitRepository, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + cmd := Command{ + &exec.Cmd{ + Path: createMockExecutable(t, tt.stdout, tt.stderr, tt.exitCode), + }, + } + + out, err := cmd.Output() + if tt.wantErr != nil { + require.Error(t, err) + var gitError *GitError + require.ErrorAs(t, err, &gitError) + assert.Equal(t, tt.wantErr, gitError.err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.stdout, string(out)) + }) + } +} + +func createMockExecutable(t *testing.T, stdout string, stderr string, exitCode int) string { + tmpDir := t.TempDir() + sourcePath := filepath.Join(tmpDir, "main.go") + binaryPath := filepath.Join(tmpDir, "mockexec") + if runtime.GOOS == "windows" { + binaryPath += ".exe" + } + + // Create Go source + source := buildCommandSourceCode(exitCode, stdout, stderr) + + // Write source file + if err := os.WriteFile(sourcePath, []byte(source), 0600); err != nil { + t.Fatal(err) + } + + // Compile + cmd := exec.Command("go", "build", "-o", binaryPath, sourcePath) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to compile: %v\n%s", err, out) + } + return binaryPath + +} + +func buildCommandSourceCode(exitCode int, stdout, stderr string) string { + return fmt.Sprintf(`package main + import ( + "fmt" + "os" + ) + func main() { + fmt.Printf(%q) + fmt.Fprintf(os.Stderr, %q) + os.Exit(%d) + }`, stdout, stderr, exitCode) +} diff --git a/git/errors.go b/git/errors.go index a3f1645aafe..9d433e906d4 100644 --- a/git/errors.go +++ b/git/errors.go @@ -7,6 +7,7 @@ import ( // ErrNotOnAnyBranch indicates that the user is in detached HEAD state. var ErrNotOnAnyBranch = errors.New("git: not on any branch") +var ErrNoGitRepository = errors.New("git: not in a git repository") type NotInstalled struct { message string diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index e4f89502c7b..0ba3ec73458 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -120,6 +120,8 @@ func (s *PullRequestRefs) GetPRHeadLabel() string { } func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) { + // If we have a URL, we don't need git stuff + if len(opts.Fields) == 0 { return nil, nil, errors.New("Find error: no fields specified") } @@ -137,37 +139,70 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err f.baseRefRepo = repo } - if f.prNumber == 0 && opts.Selector != "" { + var prRefs PullRequestRefs + if opts.Selector == "" { + // You must be in a git repo for this case to work + currentBranchName, err := f.branchFn() + if err != nil { + return nil, nil, err + } + f.branchName = currentBranchName + + // Get the branch config for the current branchName + branchConfig, err := f.branchConfig(f.branchName) + if err != nil { + return nil, nil, err + } + + // Determine if the branch is configured to merge to a special PR ref + prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`) + if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil { + prNumber, _ := strconv.Atoi(m[1]) + f.prNumber = prNumber + } + + // Determine the PullRequestRefs from config + if f.prNumber == 0 { + rems, err := f.remotesFn() + if err != nil { + return nil, nil, err + } + + // Suppressing these errors as we have other means of computing the PullRequestRefs when these fail. + parsedPushRevision, _ := f.parsePushRevision(f.branchName) + + pushDefault, err := f.pushDefault() + if err != nil { + return nil, nil, err + } + + remotePushDefault, err := f.remotePushDefault() + if err != nil { + return nil, nil, err + } + + prRefs, err = ParsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, f.baseRefRepo, rems) + if err != nil { + return nil, nil, err + } + } + + } else if f.prNumber == 0 { + // You gave me a selector but I couldn't find a PR number (it wasn't a URL) + + // Try to get a PR number from the selector + prNumber, err := strconv.Atoi(strings.TrimPrefix(opts.Selector, "#")) // If opts.Selector is a valid number then assume it is the // PR number unless opts.BaseBranch is specified. This is a // special case for PR create command which will always want // to assume that a numerical selector is a branch name rather // than PR number. - prNumber, err := strconv.Atoi(strings.TrimPrefix(opts.Selector, "#")) if opts.BaseBranch == "" && err == nil { f.prNumber = prNumber } else { f.branchName = opts.Selector + prRefs, _ = ParsePRRefs(f.branchName, git.BranchConfig{}, "", "", "", f.baseRefRepo, remotes.Remotes{}) } - } else { - currentBranchName, err := f.branchFn() - if err != nil { - return nil, nil, err - } - f.branchName = currentBranchName - } - - // Get the branch config for the current branchName - branchConfig, err := f.branchConfig(f.branchName) - if err != nil { - return nil, nil, err - } - - // Determine if the branch is configured to merge to a special PR ref - prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`) - if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil { - prNumber, _ := strconv.Atoi(m[1]) - f.prNumber = prNumber } // Set up HTTP client @@ -217,29 +252,6 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return pr, f.baseRefRepo, err } } else { - rems, err := f.remotesFn() - if err != nil { - return nil, nil, err - } - - pushDefault, err := f.pushDefault() - if err != nil { - return nil, nil, err - } - - // Suppressing these errors as we have other means of computing the PullRequestRefs when these fail. - parsedPushRevision, _ := f.parsePushRevision(f.branchName) - - remotePushDefault, err := f.remotePushDefault() - if err != nil { - return nil, nil, err - } - - prRefs, err := ParsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, f.baseRefRepo, rems) - if err != nil { - return nil, nil, err - } - pr, err = findForBranch(httpClient, f.baseRefRepo, opts.BaseBranch, prRefs.GetPRHeadLabel(), opts.States, fields.ToSlice()) if err != nil { return pr, f.baseRefRepo, err From fa5325aec943685fbdc3ff2a8f084414fb12ed03 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 31 Jan 2025 14:21:29 -0800 Subject: [PATCH 0310/2338] Add comment to status recognizing when we require a repo --- pkg/cmd/pr/status/status.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 3877fb1cf5f..2436934bfff 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -87,6 +87,7 @@ func statusRun(opts *StatusOptions) error { var currentHeadRefBranchName string if !opts.HasRepoOverride { + // We must be in a repo without the override currentBranchName, err = opts.Branch() if err != nil && !errors.Is(err, git.ErrNotOnAnyBranch) { return fmt.Errorf("could not query for pull request for current branch: %w", err) From 601cf888523f02e8c0b6abe3f03030e702b577c1 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 31 Jan 2025 14:37:19 -0800 Subject: [PATCH 0311/2338] Handle error from ParsePRRefs when the selector is provided --- pkg/cmd/pr/shared/finder.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 0ba3ec73458..332baa85775 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -201,7 +201,11 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err f.prNumber = prNumber } else { f.branchName = opts.Selector - prRefs, _ = ParsePRRefs(f.branchName, git.BranchConfig{}, "", "", "", f.baseRefRepo, remotes.Remotes{}) + // We don't expect an error here because parsedPushRevision is empty + prRefs, err = ParsePRRefs(f.branchName, git.BranchConfig{}, "", "", "", f.baseRefRepo, remotes.Remotes{}) + if err != nil { + return nil, nil, err + } } } From e428b9c66c9662541785618525d8ce3c9288ecba Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 31 Jan 2025 14:49:38 -0800 Subject: [PATCH 0312/2338] Remove unused ErrNoGitRepository error --- git/command.go | 5 ----- git/command_test.go | 12 +++++++++--- git/errors.go | 1 - 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/git/command.go b/git/command.go index b68b595252c..8065ffd86be 100644 --- a/git/command.go +++ b/git/command.go @@ -6,7 +6,6 @@ import ( "errors" "io" "os/exec" - "strings" "github.com/cli/cli/v2/internal/run" ) @@ -49,10 +48,6 @@ func (gc *Command) Output() ([]byte, error) { ge.Stderr = string(exitError.Stderr) ge.ExitCode = exitError.ExitCode() } - - if strings.Contains(ge.Stderr, "fatal: not a git repository") { - ge.err = ErrNoGitRepository - } err = &ge } return out, err diff --git a/git/command_test.go b/git/command_test.go index 15fd291fc0c..033492e01f3 100644 --- a/git/command_test.go +++ b/git/command_test.go @@ -18,7 +18,7 @@ func TestOutput(t *testing.T) { exitCode int stdout string stderr string - wantErr error + wantErr *GitError }{ { name: "successful command", @@ -32,7 +32,11 @@ func TestOutput(t *testing.T) { stdout: "", stderr: "fatal: not a git repository (or any of the parent directories): .git", exitCode: 128, - wantErr: ErrNoGitRepository, + wantErr: &GitError{ + ExitCode: 128, + Stderr: "fatal: not a git repository (or any of the parent directories): .git", + err: &exec.ExitError{}, + }, }, } @@ -50,7 +54,9 @@ func TestOutput(t *testing.T) { require.Error(t, err) var gitError *GitError require.ErrorAs(t, err, &gitError) - assert.Equal(t, tt.wantErr, gitError.err) + assert.Equal(t, tt.wantErr.ExitCode, gitError.ExitCode) + assert.Equal(t, tt.wantErr.Stderr, gitError.Stderr) + assert.Equal(t, tt.wantErr.Error(), gitError.Error()) } else { require.NoError(t, err) } diff --git a/git/errors.go b/git/errors.go index 9d433e906d4..a3f1645aafe 100644 --- a/git/errors.go +++ b/git/errors.go @@ -7,7 +7,6 @@ import ( // ErrNotOnAnyBranch indicates that the user is in detached HEAD state. var ErrNotOnAnyBranch = errors.New("git: not on any branch") -var ErrNoGitRepository = errors.New("git: not in a git repository") type NotInstalled struct { message string From 877871f1e0091ba74249693d21782b05dd4d87ba Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 31 Jan 2025 14:55:24 -0800 Subject: [PATCH 0313/2338] Address PR comments --- pkg/cmd/pr/shared/finder.go | 1 - pkg/cmd/pr/shared/finder_test.go | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 332baa85775..a5452852788 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -121,7 +121,6 @@ func (s *PullRequestRefs) GetPRHeadLabel() string { func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) { // If we have a URL, we don't need git stuff - if len(opts.Fields) == 0 { return nil, nil, errors.New("Find error: no fields specified") } diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 435377d9c43..36551ab4294 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -219,17 +219,17 @@ func TestFind(t *testing.T) { baseRepoFn: nil, branchFn: func() (string, error) { return "", &git.GitError{ - Stderr: "fatal: not a git repository (or any of the parent directories): .git", + Stderr: "fatal: branchFn error", ExitCode: 128, } }, branchConfig: stubBranchConfig(git.BranchConfig{}, &git.GitError{ - Stderr: "fatal: not a git repository (or any of the parent directories): .git", + Stderr: "fatal: branchConfig error", ExitCode: 128, }), pushDefault: stubPushDefault("simple", nil), remotePushDefault: stubRemotePushDefault("", &git.GitError{ - Stderr: "fatal: not a git repository (or any of the parent directories): .git", + Stderr: "fatal: remotePushDefault error", ExitCode: 128, }), }, From 59031c645ec004809fdfd41a8a110894a9368ec7 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sat, 1 Feb 2025 11:20:06 +1100 Subject: [PATCH 0314/2338] Revert color changes This reverts commit 36eaf14857dc8110c47b3d91b823589771aae305. --- pkg/iostreams/color.go | 2 +- pkg/iostreams/color_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 33df6bddfe5..c8d48168fa4 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -252,5 +252,5 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string { r, _ := strconv.ParseInt(hex[0:2], 16, 64) g, _ := strconv.ParseInt(hex[2:4], 16, 64) b, _ := strconv.ParseInt(hex[4:6], 16, 64) - return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[39m", r, g, b, x) + return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x) } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index f3370da2936..59fea53ed1e 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -18,7 +18,7 @@ func TestColorFromRGB(t *testing.T) { name: "truecolor", hex: "fc0303", text: "red", - wants: "\033[38;2;252;3;3mred\033[39m", + wants: "\033[38;2;252;3;3mred\033[0m", cs: NewColorScheme(true, true, true), }, { @@ -62,7 +62,7 @@ func TestHexToRGB(t *testing.T) { name: "truecolor", hex: "fc0303", text: "red", - wants: "\033[38;2;252;3;3mred\033[39m", + wants: "\033[38;2;252;3;3mred\033[0m", cs: NewColorScheme(true, true, true), }, { From 7311b5cdd12990e9e9e7b41366fd6ee4fff8ebed Mon Sep 17 00:00:00 2001 From: nilvng Date: Sat, 1 Feb 2025 11:30:25 +1100 Subject: [PATCH 0315/2338] revert isEqualSet to private --- pkg/cmd/pr/shared/finder.go | 4 ++-- pkg/cmd/pr/shared/lister.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index b1fec267ead..e4a70eb2230 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -548,7 +548,7 @@ func (m *mockFinder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, if m.expectSelector != opts.Selector { return nil, nil, fmt.Errorf("mockFinder: expected selector %q, got %q", m.expectSelector, opts.Selector) } - if len(m.expectFields) > 0 && !IsEqualSet(m.expectFields, opts.Fields) { + if len(m.expectFields) > 0 && !isEqualSet(m.expectFields, opts.Fields) { return nil, nil, fmt.Errorf("mockFinder: expected fields %v, got %v", m.expectFields, opts.Fields) } if m.called { @@ -568,7 +568,7 @@ func (m *mockFinder) ExpectFields(fields []string) { m.expectFields = fields } -func IsEqualSet(a, b []string) bool { +func isEqualSet(a, b []string) bool { if len(a) != len(b) { return false } diff --git a/pkg/cmd/pr/shared/lister.go b/pkg/cmd/pr/shared/lister.go index 004f726d572..454ecbfcf8d 100644 --- a/pkg/cmd/pr/shared/lister.go +++ b/pkg/cmd/pr/shared/lister.go @@ -183,7 +183,7 @@ func (m *mockLister) List(opt ListOptions) (*api.PullRequestAndTotalCount, error if m.expectFields != nil { - if !IsEqualSet(m.expectFields, opt.Fields) { + if !isEqualSet(m.expectFields, opt.Fields) { return nil, fmt.Errorf("unexpected fields: %v", opt.Fields) } } From 42c9d244f7ebb2a097c2c9ad81ceeabdb8427ffe Mon Sep 17 00:00:00 2001 From: nilvng Date: Sat, 1 Feb 2025 11:45:22 +1100 Subject: [PATCH 0316/2338] remove duplicated Prompter type --- pkg/cmd/pr/checkout/checkout.go | 6 +++--- pkg/cmd/pr/shared/commentable.go | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index b265ea25eab..c73d90339f7 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -28,7 +28,7 @@ type CheckoutOptions struct { Branch func() (string, error) Finder shared.PRFinder - Prompter shared.Prompter + Prompter shared.Prompt Lister shared.PRLister Interactive bool @@ -287,7 +287,7 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm return nil } -func resolvePR(baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, prLister shared.PRLister, io *iostreams.IOStreams) (*api.PullRequest, error) { +func resolvePR(baseRepo ghrepo.Interface, prompter shared.Prompt, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, prLister shared.PRLister, io *iostreams.IOStreams) (*api.PullRequest, error) { // When non-interactive if pullRequestSelector != "" { pr, _, err := pullRequestFinder.Find(shared.FindOptions{ @@ -338,7 +338,7 @@ func resolvePR(baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestS return pr, err } -func promptForPR(prompter shared.Prompter, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { +func promptForPR(prompter shared.Prompt, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { candidates := []string{} for _, pr := range jobs.PullRequests { candidates = append(candidates, fmt.Sprintf("%d\t%s %s [%s]", diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 3428986871b..7a38286d2ee 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -19,10 +19,6 @@ import ( type InputType int -type Prompter interface { - Select(string, string, []string) (int, error) -} - const ( InputTypeEditor InputType = iota InputTypeInline From 908513f97f5e311ae71a2dc69d9778513e3afbeb Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 31 Jan 2025 16:42:50 -0500 Subject: [PATCH 0317/2338] Initial working implementation --- pkg/cmd/repo/autolink/autolink.go | 3 + pkg/cmd/repo/autolink/delete/delete.go | 101 +++++++++++++++++++++++++ pkg/cmd/repo/autolink/delete/http.go | 37 +++++++++ 3 files changed, 141 insertions(+) create mode 100644 pkg/cmd/repo/autolink/delete/delete.go create mode 100644 pkg/cmd/repo/autolink/delete/http.go diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index 09b766b8513..e6c43748bfd 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -3,8 +3,10 @@ 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" "github.com/spf13/cobra" ) @@ -26,6 +28,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 } diff --git a/pkg/cmd/repo/autolink/delete/delete.go b/pkg/cmd/repo/autolink/delete/delete.go new file mode 100644 index 00000000000..9cdaaf66923 --- /dev/null +++ b/pkg/cmd/repo/autolink/delete/delete.go @@ -0,0 +1,101 @@ +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 ", + 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] + + opts.Confirmed = cmd.Flags().Changed("yes") + + 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.Confirmed { + fmt.Fprintf(out, "Autolink %s has key prefix %s.", cs.Cyan(opts.ID), autolink.KeyPrefix) + + if err := opts.Prompter.ConfirmDeletion(autolink.KeyPrefix); err != nil { + return err + } + } + + err = opts.AutolinkDeleteClient.Delete(repo, opts.ID) + if err != nil { + return err + } + + fmt.Fprintf(out, "%s Autolink %s deleted from %s\n", cs.SuccessIcon(), cs.Cyan(opts.ID), cs.Bold(ghrepo.FullName(repo))) + + return nil +} diff --git a/pkg/cmd/repo/autolink/delete/http.go b/pkg/cmd/repo/autolink/delete/http.go new file mode 100644 index 00000000000..d6bc53e840f --- /dev/null +++ b/pkg/cmd/repo/autolink/delete/http.go @@ -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 +} From e226a79dc56f95dd9a5c7d2cb0c0dbf15a11d302 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sat, 1 Feb 2025 15:46:09 -0500 Subject: [PATCH 0318/2338] Autolink delete tests --- go.mod | 1 + go.sum | 6 + pkg/cmd/repo/autolink/delete/delete_test.go | 257 ++++++++++++++++++++ pkg/cmd/repo/autolink/delete/http_test.go | 1 + pkg/cmd/repo/autolink/view/view_test.go | 16 +- 5 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 pkg/cmd/repo/autolink/delete/delete_test.go create mode 100644 pkg/cmd/repo/autolink/delete/http_test.go diff --git a/go.mod b/go.mod index 38837b88320..9dd32099f2a 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( google.golang.org/protobuf v1.36.4 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 + gotest.tools/v3 v3.0.3 ) require ( diff --git a/go.sum b/go.sum index 883ac72cefc..4e7b719c8c7 100644 --- a/go.sum +++ b/go.sum @@ -206,6 +206,7 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= @@ -360,6 +361,7 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -433,6 +435,7 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -508,6 +511,7 @@ golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCR golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= @@ -544,11 +548,13 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.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-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 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.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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= diff --git a/pkg/cmd/repo/autolink/delete/delete_test.go b/pkg/cmd/repo/autolink/delete/delete_test.go new file mode 100644 index 00000000000..99233fb1e1c --- /dev/null +++ b/pkg/cmd/repo/autolink/delete/delete_test.go @@ -0,0 +1,257 @@ +package delete + +import ( + "bytes" + "errors" + "net/http" + "testing" + + "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 + wantErr bool + errMsg string + }{ + { + name: "no argument", + input: "", + wantErr: true, + errMsg: "accepts 1 arg(s), received 0", + }, + { + name: "id provided", + input: "123", + output: deleteOptions{ID: "123"}, + }, + { + name: "yes flag", + input: "123 --yes", + output: deleteOptions{ID: "123", Confirmed: true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + 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 + stubDeleter stubAutolinkDeleter + stubViewer stubAutolinkViewer + prompterStubs func(*prompter.PrompterMock) + + wantStdout string + wantStderr string + expectedErr error + expectedErrMsg string + }{ + { + name: "delete", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + prompterStubs: func(pm *prompter.PrompterMock) { + pm.ConfirmDeletionFunc = func(_ string) error { + return nil + } + }, + wantStdout: "Autolink 123 has key prefix TICKET-.✓ Autolink 123 deleted from OWNER/REPO\n", + }, + { + name: "delete with confirm flag", + opts: &deleteOptions{ + ID: "123", + Confirmed: true, + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + wantStdout: "✓ Autolink 123 deleted from OWNER/REPO\n", + }, + { + name: "confirmation fails", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + prompterStubs: func(pm *prompter.PrompterMock) { + pm.ConfirmDeletionFunc = func(_ string) error { + return errTestPrompt + } + }, + expectedErr: errTestPrompt, + expectedErrMsg: errTestPrompt.Error(), + wantStdout: "Autolink 123 has key prefix TICKET-.", + }, + { + name: "view error", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + err: errTestAutolinkClientView, + }, + stubDeleter: stubAutolinkDeleter{ + err: nil, + }, + expectedErr: errTestAutolinkClientView, + expectedErrMsg: "error deleting autolink: autolink client view error", + }, + { + name: "delete error", + opts: &deleteOptions{ + ID: "123", + }, + stubViewer: stubAutolinkViewer{ + autolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + 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-.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + + 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") + assert.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") + }) + } +} diff --git a/pkg/cmd/repo/autolink/delete/http_test.go b/pkg/cmd/repo/autolink/delete/http_test.go new file mode 100644 index 00000000000..a67e08acc40 --- /dev/null +++ b/pkg/cmd/repo/autolink/delete/http_test.go @@ -0,0 +1 @@ +package delete diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go index bc0a1bf7f32..4192822eae2 100644 --- a/pkg/cmd/repo/autolink/view/view_test.go +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -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{}, }, From 5fea94ed9e2c9b7f7ee7789339308bc1d42b6b90 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sat, 1 Feb 2025 15:48:58 -0500 Subject: [PATCH 0319/2338] Revert change to deps --- go.mod | 1 - go.sum | 6 ------ 2 files changed, 7 deletions(-) diff --git a/go.mod b/go.mod index 9dd32099f2a..38837b88320 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,6 @@ require ( google.golang.org/protobuf v1.36.4 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 - gotest.tools/v3 v3.0.3 ) require ( diff --git a/go.sum b/go.sum index 4e7b719c8c7..883ac72cefc 100644 --- a/go.sum +++ b/go.sum @@ -206,7 +206,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= @@ -361,7 +360,6 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -435,7 +433,6 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -511,7 +508,6 @@ golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCR golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= @@ -548,13 +544,11 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.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-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 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.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= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= From fc5a6d1c3faed07267dbfa7cf4e5df577fdbbe00 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 20:09:49 -0500 Subject: [PATCH 0320/2338] Autolink delete http tests --- pkg/cmd/repo/autolink/delete/http_test.go | 68 +++++++++++++++++++++++ pkg/cmd/repo/autolink/view/http_test.go | 10 ++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/repo/autolink/delete/http_test.go b/pkg/cmd/repo/autolink/delete/http_test.go index a67e08acc40..928e561c725 100644 --- a/pkg/cmd/repo/autolink/delete/http_test.go +++ b/pkg/cmd/repo/autolink/delete/http_test.go @@ -1 +1,69 @@ 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: `{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/repos/autolinks#get-an-autolink-reference-of-a-repository", + "status": "404" + }`, + 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)", + }, + } + + 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.StatusStringResponse(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) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/view/http_test.go b/pkg/cmd/repo/autolink/view/http_test.go index 5bfe9369fc6..c47418404aa 100644 --- a/pkg/cmd/repo/autolink/view/http_test.go +++ b/pkg/cmd/repo/autolink/view/http_test.go @@ -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,7 +62,7 @@ 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", @@ -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) From dcc1efa2bd402c0d68f9b9d6aed9ebea0f782917 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 20:22:16 -0500 Subject: [PATCH 0321/2338] clean up --- pkg/cmd/repo/autolink/create/http.go | 4 ---- pkg/cmd/repo/autolink/delete/delete_test.go | 2 +- pkg/cmd/repo/autolink/view/http.go | 2 +- pkg/cmd/repo/autolink/view/http_test.go | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/http.go b/pkg/cmd/repo/autolink/create/http.go index d7ee940b907..5f187319f88 100644 --- a/pkg/cmd/repo/autolink/create/http.go +++ b/pkg/cmd/repo/autolink/create/http.go @@ -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 { diff --git a/pkg/cmd/repo/autolink/delete/delete_test.go b/pkg/cmd/repo/autolink/delete/delete_test.go index 99233fb1e1c..6dee277f835 100644 --- a/pkg/cmd/repo/autolink/delete/delete_test.go +++ b/pkg/cmd/repo/autolink/delete/delete_test.go @@ -133,7 +133,7 @@ func TestDeleteRun(t *testing.T) { return nil } }, - wantStdout: "Autolink 123 has key prefix TICKET-.✓ Autolink 123 deleted from OWNER/REPO\n", + wantStdout: "Autolink 123 has key prefix TICKET-.✓Autolink 123 deleted from OWNER/REPO\n", }, { name: "delete with confirm flag", diff --git a/pkg/cmd/repo/autolink/view/http.go b/pkg/cmd/repo/autolink/view/http.go index 8dd6dc12df2..cc5638613e4 100644 --- a/pkg/cmd/repo/autolink/view/http.go +++ b/pkg/cmd/repo/autolink/view/http.go @@ -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) } diff --git a/pkg/cmd/repo/autolink/view/http_test.go b/pkg/cmd/repo/autolink/view/http_test.go index c47418404aa..b792c975051 100644 --- a/pkg/cmd/repo/autolink/view/http_test.go +++ b/pkg/cmd/repo/autolink/view/http_test.go @@ -69,7 +69,7 @@ func TestAutolinkViewer_View(t *testing.T) { "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)", }, } From 0274999880ae9a70056e727565344ca21adba550 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 20:22:51 -0500 Subject: [PATCH 0322/2338] whitespace --- pkg/cmd/repo/autolink/autolink.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index e6c43748bfd..b4f7c6839f1 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -6,7 +6,6 @@ import ( 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" "github.com/spf13/cobra" ) From 7325944040b16eb71fb89a85f93dc50b2ba68be2 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 20:31:12 -0500 Subject: [PATCH 0323/2338] More consistency --- pkg/cmd/repo/autolink/create/create.go | 2 +- pkg/cmd/repo/autolink/delete/delete_test.go | 2 +- pkg/cmd/repo/autolink/list/list.go | 16 ++++++++-------- pkg/cmd/repo/autolink/view/view.go | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index aad71a9414f..f06ad5cf969 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -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 } diff --git a/pkg/cmd/repo/autolink/delete/delete_test.go b/pkg/cmd/repo/autolink/delete/delete_test.go index 6dee277f835..99233fb1e1c 100644 --- a/pkg/cmd/repo/autolink/delete/delete_test.go +++ b/pkg/cmd/repo/autolink/delete/delete_test.go @@ -133,7 +133,7 @@ func TestDeleteRun(t *testing.T) { return nil } }, - wantStdout: "Autolink 123 has key prefix TICKET-.✓Autolink 123 deleted from OWNER/REPO\n", + wantStdout: "Autolink 123 has key prefix TICKET-.✓ Autolink 123 deleted from OWNER/REPO\n", }, { name: "delete with confirm flag", diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 402479c44cb..7b10d558f8e 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -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,12 @@ 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 +120,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) -} diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go index 0b212aece9e..f146cd25b3d 100644 --- a/pkg/cmd/repo/autolink/view/view.go +++ b/pkg/cmd/repo/autolink/view/view.go @@ -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)) From f907ad9f97ac745d09246d85b3f7e09b0f53975c Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 20:50:37 -0500 Subject: [PATCH 0324/2338] refac and some spacing in output --- pkg/cmd/repo/autolink/delete/delete.go | 6 ++++-- pkg/cmd/repo/autolink/delete/delete_test.go | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/repo/autolink/delete/delete.go b/pkg/cmd/repo/autolink/delete/delete.go index 9cdaaf66923..70c479c2f85 100644 --- a/pkg/cmd/repo/autolink/delete/delete.go +++ b/pkg/cmd/repo/autolink/delete/delete.go @@ -83,9 +83,11 @@ func deleteRun(opts *deleteOptions) error { } if !opts.Confirmed { - fmt.Fprintf(out, "Autolink %s has key prefix %s.", cs.Cyan(opts.ID), autolink.KeyPrefix) + fmt.Fprintf(out, "Autolink %s has key prefix %s.\n", cs.Cyan(opts.ID), autolink.KeyPrefix) - if err := opts.Prompter.ConfirmDeletion(autolink.KeyPrefix); err != nil { + err := opts.Prompter.ConfirmDeletion(autolink.KeyPrefix) + + if err != nil { return err } } diff --git a/pkg/cmd/repo/autolink/delete/delete_test.go b/pkg/cmd/repo/autolink/delete/delete_test.go index 99233fb1e1c..73b7e3c775c 100644 --- a/pkg/cmd/repo/autolink/delete/delete_test.go +++ b/pkg/cmd/repo/autolink/delete/delete_test.go @@ -6,6 +6,7 @@ import ( "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" @@ -133,7 +134,10 @@ func TestDeleteRun(t *testing.T) { return nil } }, - wantStdout: "Autolink 123 has key prefix TICKET-.✓ Autolink 123 deleted from OWNER/REPO\n", + wantStdout: heredoc.Doc(` + Autolink 123 has key prefix TICKET-. + ✓ Autolink 123 deleted from OWNER/REPO + `), }, { name: "delete with confirm flag", @@ -177,7 +181,7 @@ func TestDeleteRun(t *testing.T) { }, expectedErr: errTestPrompt, expectedErrMsg: errTestPrompt.Error(), - wantStdout: "Autolink 123 has key prefix TICKET-.", + wantStdout: "Autolink 123 has key prefix TICKET-.\n", }, { name: "view error", @@ -216,7 +220,7 @@ func TestDeleteRun(t *testing.T) { }, expectedErr: errTestAutolinkClientDelete, expectedErrMsg: errTestAutolinkClientDelete.Error(), - wantStdout: "Autolink 123 has key prefix TICKET-.", + wantStdout: "Autolink 123 has key prefix TICKET-.\n", }, } From 0b3efede35f7f121ce9c0b78a194c151699e44cf Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 21:12:49 -0500 Subject: [PATCH 0325/2338] Use http constants --- pkg/cmd/repo/autolink/list/http_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index 65289c419b7..065444a89b3 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -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 { From 021557912d08ce7d168c08694d2bd67b33e9b38e Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 21:34:03 -0500 Subject: [PATCH 0326/2338] AuoLink -> Autolink --- pkg/cmd/repo/autolink/create/http_test.go | 2 +- pkg/cmd/repo/autolink/list/list_test.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/http_test.go b/pkg/cmd/repo/autolink/create/http_test.go index 9ef5e0da5ae..f5e2f53656d 100644 --- a/pkg/cmd/repo/autolink/create/http_test.go +++ b/pkg/cmd/repo/autolink/create/http_test.go @@ -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 { diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 1e4d73ab801..81acfbbdff3 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -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{}, }, From a1ae68b4f4794b838e64bf7e860abccdbfa638c5 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 21:37:34 -0500 Subject: [PATCH 0327/2338] Formatting --- pkg/cmd/repo/autolink/list/list.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 7b10d558f8e..fb726d36954 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -104,7 +104,11 @@ func listRun(opts *listOptions) error { } if opts.IO.IsStdoutTTY() { - title := fmt.Sprintf("Showing %s in %s", text.Pluralize(len(autolinks), "autolink reference"), cs.Bold(ghrepo.FullName(repo))) + 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) } From d5adafd3fb04eed62c09c0fe507f35c2292540ba Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 2 Feb 2025 21:46:56 -0500 Subject: [PATCH 0328/2338] Rename staggler --- pkg/cmd/repo/autolink/create/create_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go index 477d28da9e4..fe357f2ee83 100644 --- a/pkg/cmd/repo/autolink/create/create_test.go +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -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=", }, - 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=", 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=", }, - stubCreator: stubAutoLinkCreator{err: testAutolinkClientCreateError{}}, + stubCreator: stubAutolinkCreator{err: testAutolinkClientCreateError{}}, expectedErr: testAutolinkClientCreateError{}, errMsg: fmt.Sprint("error creating autolink: ", testAutolinkClientCreateError{}.Error()), }, From e144168dde109026d0ca4f09a158320245b77ce1 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Mon, 3 Feb 2025 15:26:05 +0500 Subject: [PATCH 0329/2338] [gh repo edit] Allow setting commit title defaults --- pkg/cmd/repo/edit/edit.go | 75 +++++++++++++---- pkg/cmd/repo/edit/edit_test.go | 142 ++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index daa700cfb68..79e32385b41 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -70,23 +70,27 @@ type EditRepositoryInput struct { enableSecretScanning *bool enableSecretScanningPushProtection *bool - AllowForking *bool `json:"allow_forking,omitempty"` - AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"` - DefaultBranch *string `json:"default_branch,omitempty"` - DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` - Description *string `json:"description,omitempty"` - EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` - EnableIssues *bool `json:"has_issues,omitempty"` - EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` - EnableProjects *bool `json:"has_projects,omitempty"` - EnableDiscussions *bool `json:"has_discussions,omitempty"` - EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` - EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` - EnableWiki *bool `json:"has_wiki,omitempty"` - Homepage *string `json:"homepage,omitempty"` - IsTemplate *bool `json:"is_template,omitempty"` - SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"` - Visibility *string `json:"visibility,omitempty"` + AllowForking *bool `json:"allow_forking,omitempty"` + AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"` + DefaultBranch *string `json:"default_branch,omitempty"` + DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` + Description *string `json:"description,omitempty"` + EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` + EnableIssues *bool `json:"has_issues,omitempty"` + EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` + MergeCommitTitle string `json:"merge_commit_title,omitempty"` + MergeCommitMessage string `json:"merge_commit_message,omitempty"` + EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` + SquashMergeCommitTitle string `json:"squash_merge_commit_title,omitempty"` + SquashMergeCommitMessage string `json:"squash_merge_commit_message,omitempty"` + EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` + EnableProjects *bool `json:"has_projects,omitempty"` + EnableDiscussions *bool `json:"has_discussions,omitempty"` + EnableWiki *bool `json:"has_wiki,omitempty"` + Homepage *string `json:"homepage,omitempty"` + IsTemplate *bool `json:"is_template,omitempty"` + SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"` + Visibility *string `json:"visibility,omitempty"` } func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command { @@ -120,6 +124,22 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr When the %[1]s--visibility%[1]s flag is used, %[1]s--accept-visibility-change-consequences%[1]s flag is required. For information on all the potential consequences, see + + For merge commit (%[1]s--enable-merge-commit%[1]s) and squash merge (%[1]s--enable-squash-merge%[1]s), + following are the valid combinations to set their respective default title and message options: + + - Merge commit (%[1]s--merge-commit-title%[1]s and %[1]s--merge-commit-message%[1]s respectively): + - MERGE_MESSAGE and PR_TITLE (default message) + - PR_TITLE and BLANK (pull request title) + - PR_TITLE and PR_BODY (pull request title and description) + + - Squash merge (%[1]s--squash-merge-commit-title%[1]s and %[1]s--squash-merge-commit-message%[1]s respectively): + - COMMIT_OR_PR_TITLE and COMMIT_MESSAGES (default message) + - PR_TITLE and BLANK (pull request title) + - PR_TITLE and PR_BODY (pull request title and description) + - PR_TITLE and COMMIT_MESSAGES (pull request title and commit details) + + Note: To set the message option, its respective title is required to be set also. `, "`"), Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` @@ -128,6 +148,12 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr # disable projects gh repo edit --enable-projects=false + + # Set merge commit default commit message (pull request title and description) + $ gh repo edit --merge-commit-title=PR_TITLE --merge-commit-message=PR_BODY + + # Set squash merge commit default message (pull request title and commit details) + $ gh repo edit --squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=COMMIT_MESSAGES `), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { @@ -166,6 +192,14 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts) } + if opts.Edits.MergeCommitMessage != "" && opts.Edits.MergeCommitTitle == "" { + return cmdutil.FlagErrorf("`--merge-commit-message` must be used in conjunction with `--merge-commit-title`") + } + + if opts.Edits.SquashMergeCommitMessage != "" && opts.Edits.SquashMergeCommitTitle == "" { + return cmdutil.FlagErrorf("`--squash-merge-commit-message` must be used in conjunction with `--squash-merge-commit-title`") + } + if runF != nil { return runF(opts) } @@ -182,8 +216,15 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableProjects, "enable-projects", "", "Enable projects in the repository") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableWiki, "enable-wiki", "", "Enable wiki in the repository") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableDiscussions, "enable-discussions", "", "Enable discussions in the repository") + cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableMergeCommit, "enable-merge-commit", "", "Enable merging pull requests via merge commit") + cmdutil.StringEnumFlag(cmd, &opts.Edits.MergeCommitTitle, "merge-commit-title", "", "", []string{"PR_TITLE", "MERGE_MESSAGE"}, "Default value for a merge commit title") + cmdutil.StringEnumFlag(cmd, &opts.Edits.MergeCommitMessage, "merge-commit-message", "", "", []string{"PR_BODY", "PR_TITLE", "BLANK"}, "Default value for a merge commit message") + cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableSquashMerge, "enable-squash-merge", "", "Enable merging pull requests via squashed commit") + cmdutil.StringEnumFlag(cmd, &opts.Edits.SquashMergeCommitTitle, "squash-merge-commit-title", "", "", []string{"PR_TITLE", "COMMIT_OR_PR_TITLE"}, "Default value for a squash merge commit title") + cmdutil.StringEnumFlag(cmd, &opts.Edits.SquashMergeCommitMessage, "squash-merge-commit-message", "", "", []string{"PR_BODY", "COMMIT_MESSAGES", "BLANK"}, "Default value for a squash merge commit message") + cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableRebaseMerge, "enable-rebase-merge", "", "Enable merging pull requests via rebase") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableAutoMerge, "enable-auto-merge", "", "Enable auto-merge functionality") cmdutil.NilBoolFlag(cmd, &opts.Edits.enableAdvancedSecurity, "enable-advanced-security", "", "Enable advanced security in the repository") diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index 868e300facd..ed4aa68c188 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -91,6 +91,145 @@ func TestNewCmdEdit(t *testing.T) { }, }, }, + { + name: "enable merge commit", + args: "--enable-merge-commit=true", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + EnableMergeCommit: bp(true), + }, + }, + }, + { + name: "disable merge commit", + args: "--enable-merge-commit=false", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + EnableMergeCommit: bp(false), + }, + }, + }, + { + name: "set merge commit default commit message (default message)", + args: "--merge-commit-title=MERGE_MESSAGE --merge-commit-message=PR_TITLE", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + MergeCommitTitle: "MERGE_MESSAGE", + MergeCommitMessage: "PR_TITLE", + }, + }, + }, + { + name: "set merge commit default commit message (pull request title)", + args: "--merge-commit-title=PR_TITLE --merge-commit-message=BLANK", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + MergeCommitTitle: "PR_TITLE", + MergeCommitMessage: "BLANK", + }, + }, + }, + { + name: "set merge commit default commit message (pull request title and description)", + args: "--merge-commit-title=PR_TITLE --merge-commit-message=PR_BODY", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + MergeCommitTitle: "PR_TITLE", + MergeCommitMessage: "PR_BODY", + }, + }, + }, + { + name: "set merge commit default commit message (message flag only)", + args: "--merge-commit-message=BLANK", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + MergeCommitMessage: "BLANK", + }, + }, + wantErr: "`--merge-commit-message` must be used in conjunction with `--merge-commit-title`", + }, + { + name: "enable squash merge commit", + args: "--enable-squash-merge=true", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + EnableSquashMerge: bp(true), + }, + }, + }, + { + name: "disable squash merge commit", + args: "--enable-squash-merge=false", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + EnableSquashMerge: bp(false), + }, + }, + }, + { + name: "set squash merge commit default commit message (default message)", + args: "--squash-merge-commit-title=COMMIT_OR_PR_TITLE --squash-merge-commit-message=COMMIT_MESSAGES", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SquashMergeCommitTitle: "COMMIT_OR_PR_TITLE", + SquashMergeCommitMessage: "COMMIT_MESSAGES", + }, + }, + }, + { + name: "set squash merge commit default commit message (pull request title)", + args: "--squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=BLANK", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SquashMergeCommitTitle: "PR_TITLE", + SquashMergeCommitMessage: "BLANK", + }, + }, + }, + { + name: "set squash merge commit default commit message (pull request title and description)", + args: "--squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=PR_BODY", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SquashMergeCommitTitle: "PR_TITLE", + SquashMergeCommitMessage: "PR_BODY", + }, + }, + }, + { + name: "set squash merge commit default commit message (pull request title and commit details)", + args: "--squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=COMMIT_MESSAGES", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SquashMergeCommitTitle: "PR_TITLE", + SquashMergeCommitMessage: "COMMIT_MESSAGES", + }, + }, + }, + { + name: "set squash merge commit default commit message (message flag only)", + args: "--squash-merge-commit-message=COMMIT_MESSAGES", + wantOpts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + Edits: EditRepositoryInput{ + SquashMergeCommitMessage: "COMMIT_MESSAGES", + }, + }, + wantErr: "`--squash-merge-commit-message` must be used in conjunction with `--squash-merge-commit-title`", + }, } for _, tt := range tests { @@ -300,7 +439,8 @@ func Test_editRun_interactive(t *testing.T) { "Template Repository", "Topics", "Visibility", - "Wikis"} + "Wikis", + } tests := []struct { name string From 6c21ab2284aed52fa92020590fdfbb9e2d8f3475 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Mon, 3 Feb 2025 11:49:21 +0000 Subject: [PATCH 0330/2338] Error when `gh repo rename` is used with a new repo name that contains an owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gh repo rename` can only rename a repo, but not change its owner (i.e. transfer it). As flagged in #10034, at the moment, the CLI behaves like it can do this, and produces weird results. If I ask to rename my repo to `polyseam/frappe-containers`: ```bash gh repo rename polyseam/frappe-containers ``` ..the preview suggests that it will do the right thing: ``` ? Rename polyseam/containers to polyseam/frappe-containers? Yes ``` ...but the repo gets renamed wrongly: ``` ✓ Renamed repository polyseam/polyseam-frappe-containers ``` This adds an upfront validation that looks for a slash and tells you that the command can't transfer a repo. Fixes #10034. --- .../repo/repo-rename-transfer-ownership.txtar | 9 +++++++++ pkg/cmd/repo/rename/rename.go | 7 ++++++- pkg/cmd/repo/rename/rename_test.go | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 acceptance/testdata/repo/repo-rename-transfer-ownership.txtar diff --git a/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar b/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar new file mode 100644 index 00000000000..6b1a3e3fbcd --- /dev/null +++ b/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar @@ -0,0 +1,9 @@ +# Create a repository with a file so it has a default branch +exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private + +# Attempt to rename the repo with a slash in the name +! exec gh repo rename $ORG/new-name --repo=$ORG/$SCRIPT_NAME-$RANDOM_STRING --yes +stderr 'New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see .' + +# Defer repo deletion +defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes \ No newline at end of file diff --git a/pkg/cmd/repo/rename/rename.go b/pkg/cmd/repo/rename/rename.go index 74523e2e7d8..172b8b552bb 100644 --- a/pkg/cmd/repo/rename/rename.go +++ b/pkg/cmd/repo/rename/rename.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" @@ -58,7 +59,7 @@ func NewCmdRename(f *cmdutil.Factory, runf func(*RenameOptions) error) *cobra.Co with %[1]s--repo%[1]s is renamed. To transfer repository ownership to another user account or organization, - you must follow additional steps on GitHub.com + you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see: @@ -124,6 +125,10 @@ func renameRun(opts *RenameOptions) error { } } + if strings.Contains(newRepoName, "/") { + return fmt.Errorf("New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see .") + } + if opts.DoConfirm { var confirmed bool if confirmed, err = opts.Prompter.Confirm(fmt.Sprintf( diff --git a/pkg/cmd/repo/rename/rename_test.go b/pkg/cmd/repo/rename/rename_test.go index da40e51b0b0..e33e14438e4 100644 --- a/pkg/cmd/repo/rename/rename_test.go +++ b/pkg/cmd/repo/rename/rename_test.go @@ -113,6 +113,8 @@ func TestRenameRun(t *testing.T) { promptStubs func(*prompter.MockPrompter) wantOut string tty bool + wantErr bool + errMsg string }{ { name: "none argument", @@ -217,6 +219,16 @@ func TestRenameRun(t *testing.T) { }, wantOut: "", }, + + { + name: "error on name with slash", + tty: true, + opts: RenameOptions{ + newRepoSelector: "org/new-name", + }, + wantErr: true, + errMsg: "New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see .", + }, } for _, tt := range testCases { @@ -268,6 +280,10 @@ func TestRenameRun(t *testing.T) { t.Run(tt.name, func(t *testing.T) { defer reg.Verify(t) err := renameRun(&tt.opts) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } assert.NoError(t, err) assert.Equal(t, tt.wantOut, stdout.String()) }) From 430e5abe2550c7a91c7c30ece2de57cb2a6889c6 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Mon, 3 Feb 2025 12:04:42 +0000 Subject: [PATCH 0331/2338] Drop accidental help text change --- pkg/cmd/repo/rename/rename.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/repo/rename/rename.go b/pkg/cmd/repo/rename/rename.go index 172b8b552bb..e50095d7626 100644 --- a/pkg/cmd/repo/rename/rename.go +++ b/pkg/cmd/repo/rename/rename.go @@ -59,7 +59,7 @@ func NewCmdRename(f *cmdutil.Factory, runf func(*RenameOptions) error) *cobra.Co with %[1]s--repo%[1]s is renamed. To transfer repository ownership to another user account or organization, - you must follow additional steps on GitHub.com. + you must follow additional steps on GitHub.com For more information on transferring repository ownership, see: From 5f54201fc5189fa41dfcb60bf6419b94fdbfa7ed Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 3 Feb 2025 15:25:15 -0500 Subject: [PATCH 0332/2338] Ensure item iteration field is populated in tests Updating tests to ensure `ProjectV2ItemFieldIterationValue` actually populates `IterationId` appropriately. --- pkg/cmd/project/shared/queries/export_data_test.go | 3 ++- pkg/cmd/project/shared/queries/queries_test.go | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/project/shared/queries/export_data_test.go b/pkg/cmd/project/shared/queries/export_data_test.go index c9f991ed44e..25e3fbace0f 100644 --- a/pkg/cmd/project/shared/queries/export_data_test.go +++ b/pkg/cmd/project/shared/queries/export_data_test.go @@ -295,6 +295,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing. iterationFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"} iterationFieldValue.ProjectV2ItemFieldIterationValue.Title = "Iteration Title" iterationFieldValue.ProjectV2ItemFieldIterationValue.Field = iterationField + iterationFieldValue.ProjectV2ItemFieldIterationValue.IterationId = "iterationId" draftIssue := ProjectItem{ Id: "draftIssueId", @@ -321,7 +322,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing. assert.NoError(t, err) assert.JSONEq( t, - `{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0,"iterationId":""},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, + `{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0,"iterationId":"iterationId"},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, string(out)) } diff --git a/pkg/cmd/project/shared/queries/queries_test.go b/pkg/cmd/project/shared/queries/queries_test.go index bfdd4d95c06..57ec9ed02f2 100644 --- a/pkg/cmd/project/shared/queries/queries_test.go +++ b/pkg/cmd/project/shared/queries/queries_test.go @@ -395,8 +395,9 @@ func TestProjectItems_FieldTitle(t *testing.T) { "fieldValues": map[string]interface{}{ "nodes": []map[string]interface{}{ { - "__typename": "ProjectV2ItemFieldIterationValue", - "title": "Iteration Title 1", + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "Iteration Title 1", + "iterationId": "iterationId1", }, { "__typename": "ProjectV2ItemFieldMilestoneValue", @@ -426,6 +427,7 @@ func TestProjectItems_FieldTitle(t *testing.T) { assert.Len(t, project.Items.Nodes, 1) assert.Len(t, project.Items.Nodes[0].FieldValues.Nodes, 2) assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.Title, "Iteration Title 1") + assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.IterationId, "iterationId1") assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[1].ProjectV2ItemFieldMilestoneValue.Milestone.Title, "Milestone Title 1") } From 800a645e0e69ad026f03abacb7e724bb5a6b0857 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 4 Feb 2025 18:16:13 +0500 Subject: [PATCH 0333/2338] [gh config] Escape pipe symbol in Long desc for website manual --- internal/docs/markdown.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go index 7ae8c6862b4..b7018c640d9 100644 --- a/internal/docs/markdown.go +++ b/internal/docs/markdown.go @@ -142,7 +142,8 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()) } if hasLong { - fmt.Fprintf(w, "%s\n\n", cmd.Long) + longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "\\|") + fmt.Fprintf(w, "%s\n\n", longWithEscapedPipe) } for _, g := range root.GroupedCommands(cmd) { From a4b86bff777791b0c07bddbde80beb5994a7f88d Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 4 Feb 2025 18:48:56 +0500 Subject: [PATCH 0334/2338] Use HTML numeric code instead of escaping with slash --- internal/docs/markdown.go | 2 +- pkg/cmd/config/config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go index b7018c640d9..05d8686b84d 100644 --- a/internal/docs/markdown.go +++ b/internal/docs/markdown.go @@ -142,7 +142,7 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()) } if hasLong { - longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "\\|") + longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "|") fmt.Fprintf(w, "%s\n\n", longWithEscapedPipe) } diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 2661c336968..66051f83a69 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -16,7 +16,7 @@ import ( func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { longDoc := strings.Builder{} longDoc.WriteString("Display or change configuration settings for gh.\n\n") - longDoc.WriteString("Current respected settings:\n") + longDoc.WriteString("Current respected settings:\n\n") for _, co := range config.Options { longDoc.WriteString(fmt.Sprintf("- `%s`: %s", co.Key, co.Description)) if len(co.AllowedValues) > 0 { From ae20633ba68634c15dc5301a7bdf48e3d59d8a16 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:55:37 -0700 Subject: [PATCH 0335/2338] Revert "[gh repo edit] Allow setting commit message defaults" --- pkg/cmd/repo/edit/edit.go | 75 ++++------------- pkg/cmd/repo/edit/edit_test.go | 142 +-------------------------------- 2 files changed, 18 insertions(+), 199 deletions(-) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 79e32385b41..daa700cfb68 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -70,27 +70,23 @@ type EditRepositoryInput struct { enableSecretScanning *bool enableSecretScanningPushProtection *bool - AllowForking *bool `json:"allow_forking,omitempty"` - AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"` - DefaultBranch *string `json:"default_branch,omitempty"` - DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` - Description *string `json:"description,omitempty"` - EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` - EnableIssues *bool `json:"has_issues,omitempty"` - EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` - MergeCommitTitle string `json:"merge_commit_title,omitempty"` - MergeCommitMessage string `json:"merge_commit_message,omitempty"` - EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` - SquashMergeCommitTitle string `json:"squash_merge_commit_title,omitempty"` - SquashMergeCommitMessage string `json:"squash_merge_commit_message,omitempty"` - EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` - EnableProjects *bool `json:"has_projects,omitempty"` - EnableDiscussions *bool `json:"has_discussions,omitempty"` - EnableWiki *bool `json:"has_wiki,omitempty"` - Homepage *string `json:"homepage,omitempty"` - IsTemplate *bool `json:"is_template,omitempty"` - SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"` - Visibility *string `json:"visibility,omitempty"` + AllowForking *bool `json:"allow_forking,omitempty"` + AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"` + DefaultBranch *string `json:"default_branch,omitempty"` + DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` + Description *string `json:"description,omitempty"` + EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` + EnableIssues *bool `json:"has_issues,omitempty"` + EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` + EnableProjects *bool `json:"has_projects,omitempty"` + EnableDiscussions *bool `json:"has_discussions,omitempty"` + EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` + EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` + EnableWiki *bool `json:"has_wiki,omitempty"` + Homepage *string `json:"homepage,omitempty"` + IsTemplate *bool `json:"is_template,omitempty"` + SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"` + Visibility *string `json:"visibility,omitempty"` } func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command { @@ -124,22 +120,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr When the %[1]s--visibility%[1]s flag is used, %[1]s--accept-visibility-change-consequences%[1]s flag is required. For information on all the potential consequences, see - - For merge commit (%[1]s--enable-merge-commit%[1]s) and squash merge (%[1]s--enable-squash-merge%[1]s), - following are the valid combinations to set their respective default title and message options: - - - Merge commit (%[1]s--merge-commit-title%[1]s and %[1]s--merge-commit-message%[1]s respectively): - - MERGE_MESSAGE and PR_TITLE (default message) - - PR_TITLE and BLANK (pull request title) - - PR_TITLE and PR_BODY (pull request title and description) - - - Squash merge (%[1]s--squash-merge-commit-title%[1]s and %[1]s--squash-merge-commit-message%[1]s respectively): - - COMMIT_OR_PR_TITLE and COMMIT_MESSAGES (default message) - - PR_TITLE and BLANK (pull request title) - - PR_TITLE and PR_BODY (pull request title and description) - - PR_TITLE and COMMIT_MESSAGES (pull request title and commit details) - - Note: To set the message option, its respective title is required to be set also. `, "`"), Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` @@ -148,12 +128,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr # disable projects gh repo edit --enable-projects=false - - # Set merge commit default commit message (pull request title and description) - $ gh repo edit --merge-commit-title=PR_TITLE --merge-commit-message=PR_BODY - - # Set squash merge commit default message (pull request title and commit details) - $ gh repo edit --squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=COMMIT_MESSAGES `), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { @@ -192,14 +166,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts) } - if opts.Edits.MergeCommitMessage != "" && opts.Edits.MergeCommitTitle == "" { - return cmdutil.FlagErrorf("`--merge-commit-message` must be used in conjunction with `--merge-commit-title`") - } - - if opts.Edits.SquashMergeCommitMessage != "" && opts.Edits.SquashMergeCommitTitle == "" { - return cmdutil.FlagErrorf("`--squash-merge-commit-message` must be used in conjunction with `--squash-merge-commit-title`") - } - if runF != nil { return runF(opts) } @@ -216,15 +182,8 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableProjects, "enable-projects", "", "Enable projects in the repository") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableWiki, "enable-wiki", "", "Enable wiki in the repository") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableDiscussions, "enable-discussions", "", "Enable discussions in the repository") - cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableMergeCommit, "enable-merge-commit", "", "Enable merging pull requests via merge commit") - cmdutil.StringEnumFlag(cmd, &opts.Edits.MergeCommitTitle, "merge-commit-title", "", "", []string{"PR_TITLE", "MERGE_MESSAGE"}, "Default value for a merge commit title") - cmdutil.StringEnumFlag(cmd, &opts.Edits.MergeCommitMessage, "merge-commit-message", "", "", []string{"PR_BODY", "PR_TITLE", "BLANK"}, "Default value for a merge commit message") - cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableSquashMerge, "enable-squash-merge", "", "Enable merging pull requests via squashed commit") - cmdutil.StringEnumFlag(cmd, &opts.Edits.SquashMergeCommitTitle, "squash-merge-commit-title", "", "", []string{"PR_TITLE", "COMMIT_OR_PR_TITLE"}, "Default value for a squash merge commit title") - cmdutil.StringEnumFlag(cmd, &opts.Edits.SquashMergeCommitMessage, "squash-merge-commit-message", "", "", []string{"PR_BODY", "COMMIT_MESSAGES", "BLANK"}, "Default value for a squash merge commit message") - cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableRebaseMerge, "enable-rebase-merge", "", "Enable merging pull requests via rebase") cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableAutoMerge, "enable-auto-merge", "", "Enable auto-merge functionality") cmdutil.NilBoolFlag(cmd, &opts.Edits.enableAdvancedSecurity, "enable-advanced-security", "", "Enable advanced security in the repository") diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index ed4aa68c188..868e300facd 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -91,145 +91,6 @@ func TestNewCmdEdit(t *testing.T) { }, }, }, - { - name: "enable merge commit", - args: "--enable-merge-commit=true", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - EnableMergeCommit: bp(true), - }, - }, - }, - { - name: "disable merge commit", - args: "--enable-merge-commit=false", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - EnableMergeCommit: bp(false), - }, - }, - }, - { - name: "set merge commit default commit message (default message)", - args: "--merge-commit-title=MERGE_MESSAGE --merge-commit-message=PR_TITLE", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - MergeCommitTitle: "MERGE_MESSAGE", - MergeCommitMessage: "PR_TITLE", - }, - }, - }, - { - name: "set merge commit default commit message (pull request title)", - args: "--merge-commit-title=PR_TITLE --merge-commit-message=BLANK", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - MergeCommitTitle: "PR_TITLE", - MergeCommitMessage: "BLANK", - }, - }, - }, - { - name: "set merge commit default commit message (pull request title and description)", - args: "--merge-commit-title=PR_TITLE --merge-commit-message=PR_BODY", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - MergeCommitTitle: "PR_TITLE", - MergeCommitMessage: "PR_BODY", - }, - }, - }, - { - name: "set merge commit default commit message (message flag only)", - args: "--merge-commit-message=BLANK", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - MergeCommitMessage: "BLANK", - }, - }, - wantErr: "`--merge-commit-message` must be used in conjunction with `--merge-commit-title`", - }, - { - name: "enable squash merge commit", - args: "--enable-squash-merge=true", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - EnableSquashMerge: bp(true), - }, - }, - }, - { - name: "disable squash merge commit", - args: "--enable-squash-merge=false", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - EnableSquashMerge: bp(false), - }, - }, - }, - { - name: "set squash merge commit default commit message (default message)", - args: "--squash-merge-commit-title=COMMIT_OR_PR_TITLE --squash-merge-commit-message=COMMIT_MESSAGES", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - SquashMergeCommitTitle: "COMMIT_OR_PR_TITLE", - SquashMergeCommitMessage: "COMMIT_MESSAGES", - }, - }, - }, - { - name: "set squash merge commit default commit message (pull request title)", - args: "--squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=BLANK", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - SquashMergeCommitTitle: "PR_TITLE", - SquashMergeCommitMessage: "BLANK", - }, - }, - }, - { - name: "set squash merge commit default commit message (pull request title and description)", - args: "--squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=PR_BODY", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - SquashMergeCommitTitle: "PR_TITLE", - SquashMergeCommitMessage: "PR_BODY", - }, - }, - }, - { - name: "set squash merge commit default commit message (pull request title and commit details)", - args: "--squash-merge-commit-title=PR_TITLE --squash-merge-commit-message=COMMIT_MESSAGES", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - SquashMergeCommitTitle: "PR_TITLE", - SquashMergeCommitMessage: "COMMIT_MESSAGES", - }, - }, - }, - { - name: "set squash merge commit default commit message (message flag only)", - args: "--squash-merge-commit-message=COMMIT_MESSAGES", - wantOpts: EditOptions{ - Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), - Edits: EditRepositoryInput{ - SquashMergeCommitMessage: "COMMIT_MESSAGES", - }, - }, - wantErr: "`--squash-merge-commit-message` must be used in conjunction with `--squash-merge-commit-title`", - }, } for _, tt := range tests { @@ -439,8 +300,7 @@ func Test_editRun_interactive(t *testing.T) { "Template Repository", "Topics", "Visibility", - "Wikis", - } + "Wikis"} tests := []struct { name string From b95e6b2ae4a4bedcd10c4aa380bb7e4ccc5df248 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 5 Feb 2025 11:22:54 +0500 Subject: [PATCH 0336/2338] Revert breaking change --- pkg/cmd/workflow/run/run.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 3ba64ad4a3d..b7ab530985e 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -262,15 +262,11 @@ func runRun(opts *RunOptions) error { ref := opts.Ref - defaultBranch, err := api.RepoDefaultBranch(client, repo) - if err != nil { - return fmt.Errorf("unable to determine default branch for %s: %w", ghrepo.FullName(repo), err) - } - - if ref != "" && ref != defaultBranch { - return fmt.Errorf("ref %s is not the default branch, and workflows must be on the default branch to be triggered by workflow_dispatch", ref) - } else { - ref = defaultBranch + if ref == "" { + ref, err = api.RepoDefaultBranch(client, repo) + if err != nil { + return fmt.Errorf("unable to determine default branch for %s: %w", ghrepo.FullName(repo), err) + } } states := []shared.WorkflowState{shared.Active} From 91c75596ee19b13fd9dea799388b80b30d5a7db0 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 5 Feb 2025 12:14:39 +0500 Subject: [PATCH 0337/2338] Address review comments --- pkg/cmd/extension/command.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index ae9a917a6cf..93c38537405 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -306,8 +306,9 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { that it's a binary extension i.e. prebuilt binaries provided as part of the release. In the absence of a release, the repository itself is cloned assuming that it's a script extension i.e. prebuilt executable or script exists on its root. + The %[1]s--pin%[1]s flag may be used to specify a tag or commit for binary and script - extensions respectively, the latest one is used otherwise. + extensions respectively, the latest version is used otherwise. For local repositories, often used while developing extensions, use %[1]s.%[1]s as the value of the repository argument. Note the following: From 50e7b00327520ffdbce9f637f54f3d3f1188c344 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 5 Feb 2025 12:21:29 +0500 Subject: [PATCH 0338/2338] Address review comments --- pkg/cmd/extension/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index e83a3ed22d0..b7f1f1c0c07 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -268,7 +268,7 @@ func (m *Manager) Install(repo ghrepo.Interface, target string) error { return err } if !hs { - return fmt.Errorf("extension is not installable: no acceptable release artifact or script found in %s", repo) + return fmt.Errorf("extension is not installable: no usable release artifact or script found in %s", repo) } return m.installGit(repo, target) From 64b63f61f667639167e89082541f23c11fca5061 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 5 Feb 2025 16:30:57 +0500 Subject: [PATCH 0339/2338] [gh project item-edit] Fix number type --- pkg/cmd/project/item-edit/item_edit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/project/item-edit/item_edit.go b/pkg/cmd/project/item-edit/item_edit.go index 63dcb3b39e9..657af1f54ac 100644 --- a/pkg/cmd/project/item-edit/item_edit.go +++ b/pkg/cmd/project/item-edit/item_edit.go @@ -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") From 277f27442ce678d578a04b3feab72aa5265a78a6 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Wed, 5 Feb 2025 19:51:52 +0500 Subject: [PATCH 0340/2338] Update tests --- pkg/cmd/project/item-edit/item_edit_test.go | 12 ++++++++++-- pkg/cmd/project/shared/queries/queries.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/project/item-edit/item_edit_test.go b/pkg/cmd/project/item-edit/item_edit_test.go index fb9097d19f5..2f9a16df691 100644 --- a/pkg/cmd/project/item-edit/item_edit_test.go +++ b/pkg/cmd/project/item-edit/item_edit_test.go @@ -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", diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index 5855e906ecc..3e63465dd08 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -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 { From 9e13890c77ac65d1b06b0032b4444e0caaf70583 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 6 Feb 2025 11:56:08 +0500 Subject: [PATCH 0341/2338] Remove short (abbreviated) flag support --- pkg/cmd/cache/delete/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index e244cc0a119..c0567417087 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -90,7 +90,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches") - cmd.Flags().BoolVarP(&opts.SucceedOnNoCaches, "succeed-on-no-caches", "s", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`") + cmd.Flags().BoolVarP(&opts.SucceedOnNoCaches, "succeed-on-no-caches", "", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`") return cmd } From 3e03f5d106b9f0e99de2c606d9b71f97ad86c556 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:17:31 +0000 Subject: [PATCH 0342/2338] Bump google.golang.org/protobuf from 1.36.4 to 1.36.5 Bumps google.golang.org/protobuf from 1.36.4 to 1.36.5. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 38837b88320..4e88a54d5f3 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 883ac72cefc..d58e8de5bc4 100644 --- a/go.sum +++ b/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= From 2f0f387e112137c0239d9605ece202a40f38504e Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 6 Feb 2025 09:47:01 -0800 Subject: [PATCH 0343/2338] Add test cases for succeed on no cache and api errors for --all (#1) --- pkg/cmd/cache/delete/delete_test.go | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go index e81fdbcec97..a9ec88deabf 100644 --- a/pkg/cmd/cache/delete/delete_test.go +++ b/pkg/cmd/cache/delete/delete_test.go @@ -176,6 +176,19 @@ func TestDeleteRun(t *testing.T) { tty: true, wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n", }, + { + name: "attempts to delete all caches but api errors", + opts: DeleteOptions{DeleteAll: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.StatusStringResponse(500, ""), + ) + }, + tty: true, + wantErr: true, + wantErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)", + }, { name: "displays delete error", opts: DeleteOptions{Identifier: "123"}, @@ -202,6 +215,54 @@ func TestDeleteRun(t *testing.T) { tty: true, wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n", }, + { + name: "no caches to delete when deleting all", + opts: DeleteOptions{Identifier: "123", DeleteAll: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.JSONResponse(shared.CachePayload{ + ActionsCaches: []shared.Cache{}, + TotalCount: 0, + }), + ) + }, + tty: false, + wantErr: true, + wantErrMsg: "X No caches to delete", + }, + { + name: "no caches to delete when deleting all but succeed on no cache tty", + opts: DeleteOptions{Identifier: "123", DeleteAll: true, SucceedOnNoCaches: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.JSONResponse(shared.CachePayload{ + ActionsCaches: []shared.Cache{}, + TotalCount: 0, + }), + ) + }, + tty: true, + wantErr: false, + wantStdout: "✓ No caches to delete\n", + }, + { + name: "no caches to delete when deleting all but succeed on no cache non-tty", + opts: DeleteOptions{Identifier: "123", DeleteAll: true, SucceedOnNoCaches: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.JSONResponse(shared.CachePayload{ + ActionsCaches: []shared.Cache{}, + TotalCount: 0, + }), + ) + }, + tty: false, + wantErr: false, + wantStdout: "", + }, } for _, tt := range tests { From 09ec38e8d776790089eac7865c6cfcf0a0e79834 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 6 Feb 2025 22:50:26 +0500 Subject: [PATCH 0344/2338] Update tests --- pkg/cmd/cache/delete/delete_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go index a9ec88deabf..57ecb9381e0 100644 --- a/pkg/cmd/cache/delete/delete_test.go +++ b/pkg/cmd/cache/delete/delete_test.go @@ -217,7 +217,7 @@ func TestDeleteRun(t *testing.T) { }, { name: "no caches to delete when deleting all", - opts: DeleteOptions{Identifier: "123", DeleteAll: true}, + opts: DeleteOptions{DeleteAll: true}, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), @@ -233,7 +233,7 @@ func TestDeleteRun(t *testing.T) { }, { name: "no caches to delete when deleting all but succeed on no cache tty", - opts: DeleteOptions{Identifier: "123", DeleteAll: true, SucceedOnNoCaches: true}, + opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), @@ -249,7 +249,7 @@ func TestDeleteRun(t *testing.T) { }, { name: "no caches to delete when deleting all but succeed on no cache non-tty", - opts: DeleteOptions{Identifier: "123", DeleteAll: true, SucceedOnNoCaches: true}, + opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), @@ -261,7 +261,7 @@ func TestDeleteRun(t *testing.T) { }, tty: false, wantErr: false, - wantStdout: "", + wantStdout: "✓ No caches to delete\n", }, } From 5d6ffa3207b1f3340fd15afb8af692cc55a3c430 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 6 Feb 2025 12:37:23 -0700 Subject: [PATCH 0345/2338] dedup local bundle err handling Signed-off-by: Meredith Lancaster --- .../attestation/verification/attestation.go | 44 ++++++++----------- .../verification/attestation_test.go | 4 +- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index fdfd667bd1b..f613ba2d51b 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -29,34 +29,28 @@ type FetchRemoteAttestationsParams struct { // GetLocalAttestations returns a slice of attestations read from a local bundle file. func GetLocalAttestations(path string) ([]*api.Attestation, error) { + var attestations []*api.Attestation + var err error fileExt := filepath.Ext(path) - switch fileExt { - case ".json": - attestations, err := loadBundleFromJSONFile(path) - if err != nil { - var pathErr *os.PathError - if errors.As(err, &pathErr) { - return nil, fmt.Errorf("bundle could not be loaded from JSON file at %s", path) - } else if errors.Is(err, bundle.ErrValidation) { - return nil, err - } - return nil, fmt.Errorf("bundle content could not be parsed") - } - return attestations, nil - case ".jsonl": - attestations, err := loadBundlesFromJSONLinesFile(path) - if err != nil { - var pathErr *os.PathError - if errors.As(err, &pathErr) { - return nil, fmt.Errorf("bundles could not be loaded from JSON lines file at %s", path) - } else if errors.Is(err, bundle.ErrValidation) { - return nil, err - } - return nil, fmt.Errorf("bundle content could not be parsed") + if fileExt == ".json" { + attestations, err = loadBundleFromJSONFile(path) + } else if fileExt == ".jsonl" { + attestations, err = loadBundlesFromJSONLinesFile(path) + } else { + return nil, ErrUnrecognisedBundleExtension + } + + if err != nil { + var pathErr *os.PathError + if errors.As(err, &pathErr) { + return nil, fmt.Errorf("could not load content from file path %s", path) + } else if errors.Is(err, bundle.ErrValidation) { + return nil, err } - return attestations, nil + return nil, fmt.Errorf("bundle content could not be parsed") } - return nil, ErrUnrecognisedBundleExtension + + return attestations, nil } func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) { diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index 08f0ccfef97..8acff0c37d2 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -92,7 +92,7 @@ func TestGetLocalAttestations(t *testing.T) { path := "../test/data/not-found-bundle.json" attestations, err := GetLocalAttestations(path) - require.ErrorContains(t, err, "bundle could not be loaded from JSON file") + require.ErrorContains(t, err, "could not load content from file path") require.Nil(t, attestations) }) @@ -100,7 +100,7 @@ func TestGetLocalAttestations(t *testing.T) { path := "../test/data/not-found-bundle.jsonl" attestations, err := GetLocalAttestations(path) - require.ErrorContains(t, err, "bundles could not be loaded from JSON lines file") + require.ErrorContains(t, err, "could not load content from file path") require.Nil(t, attestations) }) From 84299b7d576994c05aef70ff1f7abc2aab10e23b Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 6 Feb 2025 12:50:30 -0700 Subject: [PATCH 0346/2338] var naming Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index f613ba2d51b..81c8c881343 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -54,12 +54,12 @@ func GetLocalAttestations(path string) ([]*api.Attestation, error) { } func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) { - localAttestation, err := bundle.LoadJSONFromPath(path) + b, err := bundle.LoadJSONFromPath(path) if err != nil { return nil, err } - return []*api.Attestation{{Bundle: localAttestation}}, nil + return []*api.Attestation{{Bundle: b}}, nil } func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { From 60e028505d8a492c39f72d265cf241774ce31f6b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:04:04 -0700 Subject: [PATCH 0347/2338] Doc: Clarify guidelines for `help wanted` issues and prs --- .github/CONTRIBUTING.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9a0acb58ecc..01772092dc4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,23 +4,25 @@ 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: - -* 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]. - -Please avoid: - -* 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. +### 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 +* Mention `@cli/code-reviewers` when an issue you want to work on does not have clear Acceptance Criteria +* 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] + +### Please _do not_: + +* Open pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked` +* Open pull requests that haven't been approved for work in an issue +* 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 From 7fdb38028e1b52ce46b78e9e4528c019e9b91103 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 6 Feb 2025 16:06:43 -0700 Subject: [PATCH 0348/2338] remove custom transport Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/artifact/oci/client.go | 26 +--------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index bda11470814..4e5acef3c75 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "net/http" "strings" "github.com/google/go-containerregistry/pkg/authn" @@ -69,33 +68,10 @@ func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, er return &desc.Digest, name, nil } -type noncompliantRegistryTransport struct{} - -// RoundTrip will check if a request and associated response fulfill the following: -// 1. The response returns a 406 status code -// 2. The request path contains /referrers/ -// If both conditions are met, the response's status code will be overwritten to 404 -// This is a temporary solution to handle non compliant registries that return -// an unexpected status code 406 when the go-containerregistry library used -// by this code attempts to make a request to the referrers API. -// The go-containerregistry library can handle 404 response but not a 406 response. -// See the related go-containerregistry issue: https://github.com/google/go-containerregistry/issues/1962 -func (a *noncompliantRegistryTransport) RoundTrip(req *http.Request) (*http.Response, error) { - resp, err := http.DefaultTransport.RoundTrip(req) - if err != nil { - return resp, err - } - if resp.StatusCode == http.StatusNotAcceptable && strings.Contains(req.URL.Path, "/referrers/") { - resp.StatusCode = http.StatusNotFound - } - - return resp, err -} - func (c LiveClient) GetAttestations(ref name.Reference, digest string) ([]*api.Attestation, error) { attestations := make([]*api.Attestation, 0) - transportOpts := []remote.Option{remote.WithTransport(&noncompliantRegistryTransport{}), remote.WithAuthFromKeychain(authn.DefaultKeychain)} + transportOpts := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)} referrers, err := remote.Referrers(ref.Context().Digest(digest), transportOpts...) if err != nil { return attestations, fmt.Errorf("error getting referrers: %w", err) From 4cf0901b87eb6ad189d6d4eb9d4947295cf57feb Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Thu, 6 Feb 2025 18:12:42 -0500 Subject: [PATCH 0349/2338] Update release doc content - simplifying certain language - updating content around scheduled formula workflow process that makes new versions available --- docs/releasing.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index f1b87a9a726..5e3e48a984a 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -9,9 +9,7 @@ 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. +> Deployment workflow requires maintainer approval to run. What this does is: @@ -21,12 +19,13 @@ What this does is: - 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). - - The [GitHub CLI deployment workflow](.github/workflows/deployment.yml) - initiates a PR to update the `gh` Homebrew formula. - - This updated formula is then picked up by the scheduled workflow for its - inclusion in the `homebrew/homebrew-core`. + +> [!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: From 4107944f39724c1bccfa9c2f16d7f5c3ee585713 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 13:12:04 +0500 Subject: [PATCH 0350/2338] [gh api] Escape package name (URL encoding) for packages endpoint --- pkg/cmd/api/api.go | 17 +++++++++++ pkg/cmd/api/api_test.go | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index a6eb6971887..99b4fb659e0 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -264,6 +265,8 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command return err } + opts.RequestPath = escapePathWithPackageName(opts.RequestPath) + if runF != nil { return runF(&opts) } @@ -689,3 +692,17 @@ func previewNamesToMIMETypes(names []string) string { } return strings.Join(types, ", ") } + +var pathWithPackageNameRE = regexp.MustCompile(`^\/(?:orgs|user|users)(?:\/.*)?\/packages\/(?:npm|maven|rubygems|docker|nuget|container)\/(?.*?)(?:\/(?:restore|versions)|$)`) + +func escapePathWithPackageName(path string) string { + matches := pathWithPackageNameRE.FindStringSubmatch(path) + if len(matches) > 0 { + i := pathWithPackageNameRE.SubexpIndex("package") + packageName := matches[i] + if packageName != "" { + return strings.Replace(path, packageName, url.QueryEscape(packageName), 1) + } + } + return path +} diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index a565312cd55..7ba03932c63 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -367,6 +367,72 @@ func Test_NewCmdApi(t *testing.T) { }, wantsErr: false, }, + { + name: "request path with container package name containing slashes", + cli: "/user/packages/container/github.com/username/package_name --verbose", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", + Verbose: true, + }, + wantsErr: false, + }, + { + name: "request path with container package name containing slashes and restore", + cli: "/user/packages/container/github.com/username/package_name/restore --verbose", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name/restore", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", + Verbose: true, + }, + wantsErr: false, + }, + { + name: "request path with container package name containing slashes and versions", + cli: "/user/packages/container/github.com/username/package_name/versions --verbose", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name/versions", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", + Verbose: true, + }, + wantsErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From a29e74483a8cfe6ac0f0486280f436ddabbfc693 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 17:56:52 +0500 Subject: [PATCH 0351/2338] Update tests --- pkg/cmd/cache/delete/delete_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go index a9ec88deabf..57ecb9381e0 100644 --- a/pkg/cmd/cache/delete/delete_test.go +++ b/pkg/cmd/cache/delete/delete_test.go @@ -217,7 +217,7 @@ func TestDeleteRun(t *testing.T) { }, { name: "no caches to delete when deleting all", - opts: DeleteOptions{Identifier: "123", DeleteAll: true}, + opts: DeleteOptions{DeleteAll: true}, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), @@ -233,7 +233,7 @@ func TestDeleteRun(t *testing.T) { }, { name: "no caches to delete when deleting all but succeed on no cache tty", - opts: DeleteOptions{Identifier: "123", DeleteAll: true, SucceedOnNoCaches: true}, + opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), @@ -249,7 +249,7 @@ func TestDeleteRun(t *testing.T) { }, { name: "no caches to delete when deleting all but succeed on no cache non-tty", - opts: DeleteOptions{Identifier: "123", DeleteAll: true, SucceedOnNoCaches: true}, + opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), @@ -261,7 +261,7 @@ func TestDeleteRun(t *testing.T) { }, tty: false, wantErr: false, - wantStdout: "", + wantStdout: "✓ No caches to delete\n", }, } From 17e7b57b3bb31630a747e408df9b160fec5a8a94 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 18:03:35 +0500 Subject: [PATCH 0352/2338] Use API without shorthand flag --- pkg/cmd/cache/delete/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index c0567417087..a797eb6078b 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -90,7 +90,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches") - cmd.Flags().BoolVarP(&opts.SucceedOnNoCaches, "succeed-on-no-caches", "", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`") + cmd.Flags().BoolVar(&opts.SucceedOnNoCaches, "succeed-on-no-caches", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`") return cmd } From 22bac00148393308716a90271f7fe9653b3b852f Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 21:13:17 +0500 Subject: [PATCH 0353/2338] [gh pr status] Mention `gh pr checks` in the `Long` section --- pkg/cmd/pr/status/status.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 2436934bfff..b8d50659309 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -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 PRs that includes information such as + PR number, title, CI checks, reviews, etc. + + For the 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 From b2eec1cd5fd665514abf21ff77c95dc2621cf151 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 21:15:19 +0500 Subject: [PATCH 0354/2338] Update language --- pkg/cmd/pr/status/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index b8d50659309..3c02eb7b9f9 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -58,7 +58,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co The status shows a summary of PRs that includes information such as PR number, title, CI checks, reviews, etc. - For the details of CI checks, run %[1]sgh pr checks%[1]s. + 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 { From 5c6540bf59ba5d61f7fc0646e1ac6c4a0e69fd09 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:54:40 -0700 Subject: [PATCH 0355/2338] Remove redundant instructions --- .github/CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 01772092dc4..f819793f021 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,8 +11,6 @@ We accept pull requests for bug fixes and features where we've discussed the app * 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 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] ### Please _do not_: From fa39aa7e5e0ab18c5eff8ee8103b40b5c9f40ef9 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:56:14 -0700 Subject: [PATCH 0356/2338] Collapse related instructions --- .github/CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f819793f021..5bb8e43db06 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,8 +15,7 @@ We accept pull requests for bug fixes and features where we've discussed the app ### Please _do not_: -* Open pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked` -* Open pull requests that haven't been approved for work in an issue +* 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 From c8a1f8c699289bb43249eee907fcb7c5ef8601bd Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Fri, 7 Feb 2025 22:56:00 +0500 Subject: [PATCH 0357/2338] Update pkg/cmd/pr/status/status.go Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/pr/status/status.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 3c02eb7b9f9..b7b390bf27f 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -55,8 +55,8 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co Long: heredoc.Docf(` Show status of relevant pull requests. - The status shows a summary of PRs that includes information such as - PR number, title, CI checks, reviews, etc. + 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. `, "`"), From 023e3470780c2bafca6b08e6eaada56eb8f7ad77 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:24:27 -0500 Subject: [PATCH 0358/2338] Do not try to parse bodies for HEAD requests Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- pkg/cmd/api/api.go | 8 +++++--- pkg/cmd/api/api_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index a6eb6971887..a1308d09fdd 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -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 + } } } diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index a565312cd55..321f7b7c075 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -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 From 9eaaf4451630e9f8dd056ea95826bbb27e195ced Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 7 Feb 2025 16:27:04 -0500 Subject: [PATCH 0359/2338] Handle non-TTY behavior --- pkg/cmd/repo/autolink/delete/delete.go | 10 +- pkg/cmd/repo/autolink/delete/delete_test.go | 100 +++++++++++++++++--- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/repo/autolink/delete/delete.go b/pkg/cmd/repo/autolink/delete/delete.go index 70c479c2f85..e5cd874fb05 100644 --- a/pkg/cmd/repo/autolink/delete/delete.go +++ b/pkg/cmd/repo/autolink/delete/delete.go @@ -52,7 +52,9 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*deleteOptions) error) *cobra.Co opts.AutolinkViewClient = &view.AutolinkViewer{HTTPClient: httpClient} opts.ID = args[0] - opts.Confirmed = cmd.Flags().Changed("yes") + if !opts.IO.CanPrompt() && !opts.Confirmed { + return cmdutil.FlagErrorf("--yes required when not running interactively") + } if runF != nil { return runF(opts) @@ -82,7 +84,7 @@ func deleteRun(opts *deleteOptions) error { return fmt.Errorf("%s %w", cs.Red("error deleting autolink:"), err) } - if !opts.Confirmed { + 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) @@ -97,7 +99,9 @@ func deleteRun(opts *deleteOptions) error { return err } - fmt.Fprintf(out, "%s Autolink %s deleted from %s\n", cs.SuccessIcon(), cs.Cyan(opts.ID), cs.Bold(ghrepo.FullName(repo))) + 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 } diff --git a/pkg/cmd/repo/autolink/delete/delete_test.go b/pkg/cmd/repo/autolink/delete/delete_test.go index 73b7e3c775c..7606ebe333e 100644 --- a/pkg/cmd/repo/autolink/delete/delete_test.go +++ b/pkg/cmd/repo/autolink/delete/delete_test.go @@ -23,29 +23,51 @@ func TestNewCmdDelete(t *testing.T) { 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, } @@ -104,12 +126,12 @@ func TestDeleteRun(t *testing.T) { tests := []struct { name string opts *deleteOptions + isTTY bool stubDeleter stubAutolinkDeleter stubViewer stubAutolinkViewer prompterStubs func(*prompter.PrompterMock) wantStdout string - wantStderr string expectedErr error expectedErrMsg string }{ @@ -118,6 +140,7 @@ func TestDeleteRun(t *testing.T) { opts: &deleteOptions{ ID: "123", }, + isTTY: true, stubViewer: stubAutolinkViewer{ autolink: &shared.Autolink{ ID: 123, @@ -145,6 +168,7 @@ func TestDeleteRun(t *testing.T) { ID: "123", Confirmed: true, }, + isTTY: true, stubViewer: stubAutolinkViewer{ autolink: &shared.Autolink{ ID: 123, @@ -153,16 +177,15 @@ func TestDeleteRun(t *testing.T) { IsAlphanumeric: true, }, }, - stubDeleter: stubAutolinkDeleter{ - err: nil, - }, - wantStdout: "✓ Autolink 123 deleted from OWNER/REPO\n", + 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, @@ -171,9 +194,7 @@ func TestDeleteRun(t *testing.T) { IsAlphanumeric: true, }, }, - stubDeleter: stubAutolinkDeleter{ - err: nil, - }, + stubDeleter: stubAutolinkDeleter{}, prompterStubs: func(pm *prompter.PrompterMock) { pm.ConfirmDeletionFunc = func(_ string) error { return errTestPrompt @@ -188,12 +209,11 @@ func TestDeleteRun(t *testing.T) { opts: &deleteOptions{ ID: "123", }, + isTTY: true, stubViewer: stubAutolinkViewer{ err: errTestAutolinkClientView, }, - stubDeleter: stubAutolinkDeleter{ - err: nil, - }, + stubDeleter: stubAutolinkDeleter{}, expectedErr: errTestAutolinkClientView, expectedErrMsg: "error deleting autolink: autolink client view error", }, @@ -202,6 +222,7 @@ func TestDeleteRun(t *testing.T) { opts: &deleteOptions{ ID: "123", }, + isTTY: true, stubViewer: stubAutolinkViewer{ autolink: &shared.Autolink{ ID: 123, @@ -222,11 +243,65 @@ func TestDeleteRun(t *testing.T) { 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=", + 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=", + 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, stderr := iostreams.Test() + ios, _, stdout, _ := iostreams.Test() + + ios.SetStdinTTY(tt.isTTY) + ios.SetStdoutTTY(tt.isTTY) opts := tt.opts opts.IO = ios @@ -255,7 +330,6 @@ func TestDeleteRun(t *testing.T) { } assert.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") - assert.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") }) } } From 8e2be7326b5fe966aeef4697c8f17d6ab3fc42c4 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 7 Feb 2025 16:51:19 -0500 Subject: [PATCH 0360/2338] Improve http error test cases --- pkg/cmd/repo/autolink/delete/http_test.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/repo/autolink/delete/http_test.go b/pkg/cmd/repo/autolink/delete/http_test.go index 928e561c725..a2676178db4 100644 --- a/pkg/cmd/repo/autolink/delete/http_test.go +++ b/pkg/cmd/repo/autolink/delete/http_test.go @@ -28,17 +28,21 @@ func TestAutolinkDeleter_Delete(t *testing.T) { stubStatus: http.StatusNoContent, }, { - name: "404 repo or autolink not found", - id: "123", - 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" - }`, + 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 { @@ -49,7 +53,7 @@ func TestAutolinkDeleter_Delete(t *testing.T) { http.MethodDelete, fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id), ), - httpmock.StatusStringResponse(tt.stubStatus, tt.stubRespJSON), + httpmock.StatusJSONResponse(tt.stubStatus, tt.stubRespJSON), ) defer reg.Verify(t) From 0268d95f561a1e225ed19b381be9c37c635cbf11 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:00:45 -0700 Subject: [PATCH 0361/2338] Fix logic error in contributing docs --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5bb8e43db06..40d0a83e3cb 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,7 +11,7 @@ We accept pull requests for bug fixes and features where we've discussed the app * 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] or [`good first issue`][gfi] +* Open a pull request for any issue labelled [`help wanted`][hw] and [`good first issue`][gfi] ### Please _do not_: From 49fd7a57564d7ed2525eaee3284959e3672952f7 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Sat, 8 Feb 2025 18:20:43 +0500 Subject: [PATCH 0362/2338] [gh release create] Fail when there are no new commits since the most recent release --- pkg/cmd/release/create/create.go | 12 +++ pkg/cmd/release/create/create_test.go | 110 ++++++++++++++++++++++++++ pkg/cmd/release/create/http.go | 46 +++++++++++ pkg/cmd/release/shared/fetch.go | 8 +- 4 files changed, 172 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 8e77546d0a7..dddd3066fda 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -60,6 +60,7 @@ type CreateOptions struct { NotesStartTag string VerifyTag bool NotesFromTag bool + OnlyNew bool } func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { @@ -194,6 +195,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co cmdutil.NilBoolFlag(cmd, &opts.IsLatest, "latest", "", "Mark this release as \"Latest\" (default [automatic based on date and version]). --latest=false to explicitly NOT set as latest") cmd.Flags().BoolVarP(&opts.VerifyTag, "verify-tag", "", false, "Abort in case the git tag doesn't already exist in the remote repository") cmd.Flags().BoolVarP(&opts.NotesFromTag, "notes-from-tag", "", false, "Automatically generate notes from annotated tag") + cmd.Flags().BoolVar(&opts.OnlyNew, "only-new", false, "Create release only if there are new commits since the last release") _ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "target") @@ -211,6 +213,16 @@ func createRun(opts *CreateOptions) error { return err } + if opts.OnlyNew { + isNew, err := isNewRelease(httpClient, baseRepo) + if err != nil { + return err + } + if !isNew { + return fmt.Errorf("no new commits since the last release") + } + } + var existingTag bool if opts.TagName == "" { tags, err := getTags(httpClient, baseRepo, 5) diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index c9e9a8c8a59..252d28cbced 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -63,6 +63,7 @@ func Test_NewCmdCreate(t *testing.T) { Concurrency: 5, Assets: []*shared.AssetForUpload(nil), VerifyTag: false, + OnlyNew: false, }, }, { @@ -87,6 +88,7 @@ func Test_NewCmdCreate(t *testing.T) { Concurrency: 5, Assets: []*shared.AssetForUpload(nil), VerifyTag: false, + OnlyNew: false, }, }, { @@ -347,6 +349,19 @@ func Test_NewCmdCreate(t *testing.T) { isTTY: false, wantErr: "using `--notes-from-tag` with `--generate-notes` or `--notes-start-tag` is not supported", }, + { + name: "with --only-new", + args: "v1.2.3 --only-new", + isTTY: false, + want: CreateOptions{ + TagName: "v1.2.3", + BodyProvided: false, + Concurrency: 5, + Assets: []*shared.AssetForUpload(nil), + NotesFromTag: false, + OnlyNew: true, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -402,6 +417,7 @@ func Test_NewCmdCreate(t *testing.T) { assert.Equal(t, tt.want.IsLatest, opts.IsLatest) assert.Equal(t, tt.want.VerifyTag, opts.VerifyTag) assert.Equal(t, tt.want.NotesFromTag, opts.NotesFromTag) + assert.Equal(t, tt.want.OnlyNew, opts.OnlyNew) require.Equal(t, len(tt.want.Assets), len(opts.Assets)) for i := range tt.want.Assets { @@ -460,6 +476,100 @@ func Test_createRun(t *testing.T) { wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", wantStderr: ``, }, + { + name: "create a release if there are new commits and the last release does not exist", + isTTY: true, + opts: CreateOptions{ + TagName: "v1.2.3", + Name: "The Big 1.2", + Body: "* Fixed bugs", + BodyProvided: true, + Target: "", + OnlyNew: true, + }, + runStubs: defaultRunStubs, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(404, `{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/releases/releases#get-the-latest-release", + "status": "404" + }`)) + reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ + "url": "https://api.github.com/releases/123", + "upload_url": "https://api.github.com/assets/upload", + "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" + }`, func(params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "tag_name": "v1.2.3", + "name": "The Big 1.2", + "body": "* Fixed bugs", + "draft": false, + "prerelease": false, + }, params) + })) + }, + wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", + wantStderr: ``, + }, + { + name: "create a release if there are new commits and the last release exists", + isTTY: true, + opts: CreateOptions{ + TagName: "v1.2.3", + Name: "The Big 1.2", + Body: "* Fixed bugs", + BodyProvided: true, + Target: "", + OnlyNew: true, + }, + runStubs: defaultRunStubs, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{ + "tag_name": "v1.2.2" + }`)) + reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{ + "status": "ahead" + }`)) + reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{ + "url": "https://api.github.com/releases/123", + "upload_url": "https://api.github.com/assets/upload", + "html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3" + }`, func(params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "tag_name": "v1.2.3", + "name": "The Big 1.2", + "body": "* Fixed bugs", + "draft": false, + "prerelease": false, + }, params) + })) + }, + wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n", + wantStderr: ``, + }, + { + name: "create a release if there are no new commits but the last release exists", + isTTY: true, + opts: CreateOptions{ + TagName: "v1.2.3", + Name: "The Big 1.2", + Body: "* Fixed bugs", + BodyProvided: true, + Target: "", + OnlyNew: true, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{ + "tag_name": "v1.2.2" + }`)) + reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{ + "status": "identical" + }`)) + }, + wantErr: "no new commits since the last release", + wantStdout: "", + wantStderr: ``, + }, { name: "with discussion category", isTTY: true, diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index 3bb55f39e64..0db8b1c0ac1 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -2,6 +2,7 @@ package create import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -299,3 +300,48 @@ func tokenHasWorkflowScope(resp *http.Response) bool { return slices.Contains(strings.Split(scopes, ","), "workflow") } + +// isNewRelease checks if there are new commits since the latest release. +func isNewRelease(httpClient *http.Client, repo ghrepo.Interface) (bool, error) { + ctx := context.Background() + release, err := shared.FetchLatestRelease(ctx, httpClient, repo) + if err != nil { + if errors.Is(err, shared.ErrReleaseNotFound) { + return true, nil + } else { + return false, err + } + } + + tagName := release.TagName + path := fmt.Sprintf("repos/%s/%s/compare/%s...HEAD?per_page=1", repo.RepoOwner(), repo.RepoName(), tagName) + url := ghinstance.RESTPrefix(repo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + resp, err := httpClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return false, api.HandleHTTPError(resp) + } + + type comparisonStatus struct { + Status string `json:"status"` + } + + var cmpStatus comparisonStatus + if err := json.NewDecoder(resp.Body).Decode(&cmpStatus); err != nil { + return false, err + } + + isNew := cmpStatus.Status == "ahead" + return isNew, nil +} diff --git a/pkg/cmd/release/shared/fetch.go b/pkg/cmd/release/shared/fetch.go index c9cf7a6a413..8db7e502ace 100644 --- a/pkg/cmd/release/shared/fetch.go +++ b/pkg/cmd/release/shared/fetch.go @@ -124,7 +124,7 @@ func (rel *Release) ExportData(fields []string) map[string]interface{} { return data } -var errNotFound = errors.New("release not found") +var ErrReleaseNotFound = errors.New("release not found") type fetchResult struct { release *Release @@ -150,7 +150,7 @@ func FetchRelease(ctx context.Context, httpClient *http.Client, repo ghrepo.Inte }() res := <-results - if errors.Is(res.error, errNotFound) { + if errors.Is(res.error, ErrReleaseNotFound) { res = <-results cancel() // satisfy the linter even though no goroutines are running anymore } else { @@ -190,7 +190,7 @@ func fetchDraftRelease(ctx context.Context, httpClient *http.Client, repo ghrepo } if query.Repository.Release == nil || !query.Repository.Release.IsDraft { - return nil, errNotFound + return nil, ErrReleaseNotFound } // Then, use REST to get information about the draft release. In theory, we could have fetched @@ -213,7 +213,7 @@ func fetchReleasePath(ctx context.Context, httpClient *http.Client, host string, if resp.StatusCode == 404 { _, _ = io.Copy(io.Discard, resp.Body) - return nil, errNotFound + return nil, ErrReleaseNotFound } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } From 161ad92b20631ee8e367b59d0018ccfbf4dc3c9d Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Sun, 9 Feb 2025 21:58:15 +0500 Subject: [PATCH 0363/2338] [gh release create/upload] Expand glob patterns on Windows --- pkg/cmd/release/shared/upload.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pkg/cmd/release/shared/upload.go b/pkg/cmd/release/shared/upload.go index 38add742566..10da2e55620 100644 --- a/pkg/cmd/release/shared/upload.go +++ b/pkg/cmd/release/shared/upload.go @@ -4,12 +4,15 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "mime" "net/http" "net/url" "os" "path" + "path/filepath" + "runtime" "strings" "time" @@ -36,6 +39,12 @@ type AssetForUpload struct { } func AssetsFromArgs(args []string) (assets []*AssetForUpload, err error) { + if runtime.GOOS == "windows" { + args, err = globAssetPatterns(args) + if err != nil { + return nil, err + } + } for _, arg := range args { var label string fn := arg @@ -63,6 +72,22 @@ func AssetsFromArgs(args []string) (assets []*AssetForUpload, err error) { return } +func globAssetPatterns(patterns []string) ([]string, error) { + var assets []string + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, fmt.Errorf("%s: %s", pattern, err) + } + if len(matches) > 0 { + assets = append(assets, matches...) + } else { + assets = append(assets, pattern) + } + } + return assets, nil +} + func typeForFilename(fn string) string { ext := fileExt(fn) switch ext { From 81eeaca959fda04f6482f5aaf2d37f0fa130dd29 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 10 Feb 2025 17:24:43 -0500 Subject: [PATCH 0364/2338] Align no default repo message with other uses --- context/context.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/context/context.go b/context/context.go index e3b84e6b195..7374e02bb78 100644 --- a/context/context.go +++ b/context/context.go @@ -99,13 +99,13 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e cs := io.ColorScheme() fmt.Fprintf(io.ErrOut, - "%s No default remote repository has been set for this directory.\n", + "%s No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n", cs.FailureIcon()) fmt.Fprintln(io.Out) return nil, errors.New( - "please run `gh repo set-default` to select a default remote repository.\nTo learn more about the default repository, run: gh repo set-default --help") + "please run `gh repo set-default` to select a default remote repository.") } func (r *ResolvedRemotes) HeadRepos() ([]*api.Repository, error) { From cdc7cae29c5dc3c617d978f366e5b0ac4a8dccca Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 11 Feb 2025 09:35:16 +0500 Subject: [PATCH 0365/2338] Address review comments --- pkg/cmd/repo/clone/clone.go | 2 +- pkg/cmd/repo/fork/fork.go | 4 ++-- pkg/cmd/repo/setdefault/setdefault.go | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index 1466cd96a0d..f2b96fa12fa 100644 --- a/pkg/cmd/repo/clone/clone.go +++ b/pkg/cmd/repo/clone/clone.go @@ -217,7 +217,7 @@ func cloneRun(opts *CloneOptions) error { connectedToTerminal := opts.IO.IsStdoutTTY() if connectedToTerminal { cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.ErrOut, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(canonicalRepo.Parent))) + fmt.Fprintf(opts.IO.Out, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(canonicalRepo.Parent))) } } return nil diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 3d62a73cceb..2da807ee316 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -378,8 +378,8 @@ func forkRun(opts *ForkOptions) error { } if connectedToTerminal { - fmt.Fprintf(stderr, "%s Cloned fork\n", cs.SuccessIcon()) - fmt.Fprintf(stderr, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(repoToFork))) + fmt.Fprintf(opts.IO.Out, "%s Cloned fork\n", cs.SuccessIcon()) + fmt.Fprintf(opts.IO.Out, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(repoToFork))) } } } diff --git a/pkg/cmd/repo/setdefault/setdefault.go b/pkg/cmd/repo/setdefault/setdefault.go index 3e1554a6933..ede4d00b4b0 100644 --- a/pkg/cmd/repo/setdefault/setdefault.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -119,15 +119,18 @@ func setDefaultRun(opts *SetDefaultOptions) error { currentDefaultRepo, _ := remotes.ResolvedRemote() + cs := opts.IO.ColorScheme() + if opts.ViewMode { if currentDefaultRepo != nil { fmt.Fprintln(opts.IO.Out, displayRemoteRepoName(currentDefaultRepo)) } else { - fmt.Fprintln(opts.IO.ErrOut, "no default repository has been set; use `gh repo set-default` to select one") + fmt.Fprintf(opts.IO.ErrOut, + "%s No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n", + cs.FailureIcon()) } return nil } - cs := opts.IO.ColorScheme() if opts.UnsetMode { var msg string From eb44ec2d869255675232d878ea100930b0d11a37 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 11 Feb 2025 10:34:10 +0500 Subject: [PATCH 0366/2338] Update tests --- pkg/cmd/repo/clone/clone.go | 2 +- pkg/cmd/repo/fork/fork.go | 4 ++-- pkg/cmd/repo/setdefault/setdefault_test.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index f2b96fa12fa..1466cd96a0d 100644 --- a/pkg/cmd/repo/clone/clone.go +++ b/pkg/cmd/repo/clone/clone.go @@ -217,7 +217,7 @@ func cloneRun(opts *CloneOptions) error { connectedToTerminal := opts.IO.IsStdoutTTY() if connectedToTerminal { cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(canonicalRepo.Parent))) + fmt.Fprintf(opts.IO.ErrOut, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(canonicalRepo.Parent))) } } return nil diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 2da807ee316..3d62a73cceb 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -378,8 +378,8 @@ func forkRun(opts *ForkOptions) error { } if connectedToTerminal { - fmt.Fprintf(opts.IO.Out, "%s Cloned fork\n", cs.SuccessIcon()) - fmt.Fprintf(opts.IO.Out, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(repoToFork))) + fmt.Fprintf(stderr, "%s Cloned fork\n", cs.SuccessIcon()) + fmt.Fprintf(stderr, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(repoToFork))) } } } diff --git a/pkg/cmd/repo/setdefault/setdefault_test.go b/pkg/cmd/repo/setdefault/setdefault_test.go index dfd72d83e76..0d2e2ddaacc 100644 --- a/pkg/cmd/repo/setdefault/setdefault_test.go +++ b/pkg/cmd/repo/setdefault/setdefault_test.go @@ -176,7 +176,7 @@ func TestDefaultRun(t *testing.T) { Repo: repo1, }, }, - wantStderr: "no default repository has been set; use `gh repo set-default` to select one\n", + wantStderr: "X No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n", }, { name: "view mode no current default", @@ -188,7 +188,7 @@ func TestDefaultRun(t *testing.T) { Repo: repo1, }, }, - wantStderr: "no default repository has been set; use `gh repo set-default` to select one\n", + wantStderr: "X No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n", }, { name: "view mode with base resolved current default", From d9bfa606ccce89fce82b5a7809423cd6157a7838 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Tue, 11 Feb 2025 12:56:32 +0500 Subject: [PATCH 0367/2338] Fix `gh gist create` for multiple filenames and glob patterns --- pkg/cmd/gist/create/create.go | 14 +++++++++----- pkg/cmd/release/shared/upload.go | 28 ++++------------------------ pkg/cmdutil/args.go | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/pkg/cmd/gist/create/create.go b/pkg/cmd/gist/create/create.go index 4403df9be79..ad697e2ce56 100644 --- a/pkg/cmd/gist/create/create.go +++ b/pkg/cmd/gist/create/create.go @@ -85,7 +85,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co }, Aliases: []string{"new"}, RunE: func(c *cobra.Command, args []string) error { - opts.Filenames = args + opts.Filenames = args[:] if runF != nil { return runF(&opts) @@ -102,12 +102,16 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } func createRun(opts *CreateOptions) error { - fileArgs := opts.Filenames - if len(fileArgs) == 0 { - fileArgs = []string{"-"} + filenames, err := cmdutil.GlobWindowsPaths(opts.Filenames) + if err != nil { + return err + } + + if len(filenames) == 0 { + filenames = []string{"-"} } - files, err := processFiles(opts.IO.In, opts.FilenameOverride, fileArgs) + files, err := processFiles(opts.IO.In, opts.FilenameOverride, filenames) if err != nil { return fmt.Errorf("failed to collect files for posting: %w", err) } diff --git a/pkg/cmd/release/shared/upload.go b/pkg/cmd/release/shared/upload.go index 10da2e55620..159f86e2377 100644 --- a/pkg/cmd/release/shared/upload.go +++ b/pkg/cmd/release/shared/upload.go @@ -4,20 +4,18 @@ import ( "context" "encoding/json" "errors" - "fmt" "io" "mime" "net/http" "net/url" "os" "path" - "path/filepath" - "runtime" "strings" "time" "github.com/cenkalti/backoff/v4" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/pkg/cmdutil" "golang.org/x/sync/errgroup" ) @@ -39,11 +37,9 @@ type AssetForUpload struct { } func AssetsFromArgs(args []string) (assets []*AssetForUpload, err error) { - if runtime.GOOS == "windows" { - args, err = globAssetPatterns(args) - if err != nil { - return nil, err - } + args, err = cmdutil.GlobWindowsPaths(args) + if err != nil { + return nil, err } for _, arg := range args { var label string @@ -72,22 +68,6 @@ func AssetsFromArgs(args []string) (assets []*AssetForUpload, err error) { return } -func globAssetPatterns(patterns []string) ([]string, error) { - var assets []string - for _, pattern := range patterns { - matches, err := filepath.Glob(pattern) - if err != nil { - return nil, fmt.Errorf("%s: %s", pattern, err) - } - if len(matches) > 0 { - assets = append(assets, matches...) - } else { - assets = append(assets, pattern) - } - } - return assets, nil -} - func typeForFilename(fn string) string { ext := fileExt(fn) switch ext { diff --git a/pkg/cmdutil/args.go b/pkg/cmdutil/args.go index 0f03a07bd71..9dfbce2b1ad 100644 --- a/pkg/cmdutil/args.go +++ b/pkg/cmdutil/args.go @@ -2,6 +2,8 @@ package cmdutil import ( "fmt" + "path/filepath" + "runtime" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -57,3 +59,22 @@ func NoArgsQuoteReminder(cmd *cobra.Command, args []string) error { return FlagErrorf("%s", errMsg) } + +func GlobWindowsPaths(patterns []string) ([]string, error) { + if runtime.GOOS != "windows" { + return patterns, nil + } + var expansions []string + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, fmt.Errorf("%s: %s", pattern, err) + } + if len(matches) > 0 { + expansions = append(expansions, matches...) + } else { + expansions = append(expansions, pattern) + } + } + return expansions, nil +} From bf3a40aef3af6919bba73bfeaadac2d0c169628d Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 11 Feb 2025 09:07:51 +0100 Subject: [PATCH 0368/2338] Exit with error if no matching predicate type exists Signed-off-by: Fredrik Skogman --- pkg/cmd/attestation/verify/verify.go | 2 +- pkg/cmd/attestation/verify/verify_test.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 90242a9fed2..0a8de8b4599 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -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 diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 87ffa96f090..092a009d81e 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -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" From e516e5ed5db056653dbb1dd141e4c714555a5967 Mon Sep 17 00:00:00 2001 From: latzskim Date: Wed, 12 Feb 2025 21:04:22 +0100 Subject: [PATCH 0369/2338] [gh issue/pr comment] Create a comment if no comment already --- pkg/cmd/issue/comment/comment.go | 14 +-- pkg/cmd/issue/comment/comment_test.go | 127 ++++++++++++++++++++++--- pkg/cmd/pr/comment/comment.go | 14 +-- pkg/cmd/pr/comment/comment_test.go | 131 +++++++++++++++++++++++--- pkg/cmd/pr/shared/commentable.go | 61 +++++++++--- 5 files changed, 297 insertions(+), 50 deletions(-) diff --git a/pkg/cmd/issue/comment/comment.go b/pkg/cmd/issue/comment/comment.go index c91cf79c943..090b0748c4b 100644 --- a/pkg/cmd/issue/comment/comment.go +++ b/pkg/cmd/issue/comment/comment.go @@ -11,12 +11,13 @@ import ( func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) error) *cobra.Command { opts := &prShared.CommentableOptions{ - IO: f.IOStreams, - HttpClient: f.HttpClient, - EditSurvey: prShared.CommentableEditSurvey(f.Config, f.IOStreams), - InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams), - ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey(f.Prompter), - OpenInBrowser: f.Browser.Browse, + IO: f.IOStreams, + HttpClient: f.HttpClient, + EditSurvey: prShared.CommentableEditSurvey(f.Config, f.IOStreams), + InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams), + ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey(f.Prompter), + ConfirmCreateIfNoneSurvey: prShared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter), + OpenInBrowser: f.Browser.Browse, } var bodyFile string @@ -69,6 +70,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in") cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment") cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the same author") + cmd.Flags().BoolVar(&opts.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last") return cmd } diff --git a/pkg/cmd/issue/comment/comment_test.go b/pkg/cmd/issue/comment/comment_test.go index 668d758e58c..461ae1065e1 100644 --- a/pkg/cmd/issue/comment/comment_test.go +++ b/pkg/cmd/issue/comment/comment_test.go @@ -109,6 +109,29 @@ func TestNewCmdComment(t *testing.T) { }, wantsErr: false, }, + { + name: "edit last flag", + input: "1 --edit-last", + output: shared.CommentableOptions{ + Interactive: true, + InputType: shared.InputTypeEditor, + Body: "", + EditLast: true, + }, + wantsErr: false, + }, + { + name: "edit last flag with create if none", + input: "1 --edit-last --create-if-none", + output: shared.CommentableOptions{ + Interactive: true, + InputType: shared.InputTypeEditor, + Body: "", + EditLast: true, + CreateIfNone: true, + }, + wantsErr: false, + }, { name: "body and body-file flags", input: "1 --body 'test' --body-file 'test-file.txt'", @@ -139,6 +162,12 @@ func TestNewCmdComment(t *testing.T) { output: shared.CommentableOptions{}, wantsErr: true, }, + { + name: "create-if-none flag without edit-last", + input: "1 --create-if-none", + output: shared.CommentableOptions{}, + wantsErr: true, + }, } for _, tt := range tests { @@ -188,11 +217,12 @@ func TestNewCmdComment(t *testing.T) { func Test_commentRun(t *testing.T) { tests := []struct { - name string - input *shared.CommentableOptions - httpStubs func(*testing.T, *httpmock.Registry) - stdout string - stderr string + name string + input *shared.CommentableOptions + emptyComments bool + httpStubs func(*testing.T, *httpmock.Registry) + stdout string + stderr string }{ { name: "interactive editor", @@ -225,6 +255,24 @@ func Test_commentRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n", }, + { + name: "interactive editor with edit last and create if none", + input: &shared.CommentableOptions{ + Interactive: true, + InputType: 0, + Body: "", + EditLast: true, + CreateIfNone: true, + + InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil }, + ConfirmCreateIfNoneSurvey: func() (bool, error) { return true, nil }, + ConfirmSubmitSurvey: func() (bool, error) { return true, nil }, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + mockCommentUpdate(t, reg) + }, + stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n", + }, { name: "non-interactive web", input: &shared.CommentableOptions{ @@ -248,6 +296,39 @@ func Test_commentRun(t *testing.T) { }, stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n", }, + { + name: "non-interactive web with edit last and create if none for empty comments", + input: &shared.CommentableOptions{ + Interactive: false, + InputType: shared.InputTypeWeb, + Body: "", + EditLast: true, + CreateIfNone: true, + + OpenInBrowser: func(u string) error { + assert.Contains(t, u, "#issuecomment-new") + return nil + }, + }, + emptyComments: true, + stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n", + }, + { + name: "non-interactive web with edit last and create if none", + input: &shared.CommentableOptions{ + Interactive: false, + InputType: shared.InputTypeWeb, + Body: "", + EditLast: true, + CreateIfNone: true, + + OpenInBrowser: func(u string) error { + assert.Contains(t, u, "#issuecomment-111") + return nil + }, + }, + stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n", + }, { name: "non-interactive editor", input: &shared.CommentableOptions{ @@ -277,6 +358,23 @@ func Test_commentRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n", }, + { + name: "non-interactive editor with edit last and create if none", + input: &shared.CommentableOptions{ + Interactive: false, + InputType: shared.InputTypeEditor, + Body: "", + EditLast: true, + CreateIfNone: true, + + EditSurvey: func(string) (string, error) { return "comment body", nil }, + }, + emptyComments: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + mockCommentCreate(t, reg) + }, + stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n", + }, { name: "non-interactive inline", input: &shared.CommentableOptions{ @@ -319,14 +417,21 @@ func Test_commentRun(t *testing.T) { tt.input.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } + + comments := api.Comments{Nodes: []api.Comment{ + {ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-111", ViewerDidAuthor: true}, + {ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-222"}, + }} + + if tt.emptyComments { + comments.Nodes = []api.Comment{} + } + tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) { return &api.Issue{ - ID: "ISSUE-ID", - URL: "https://github.com/OWNER/REPO/issues/123", - Comments: api.Comments{Nodes: []api.Comment{ - {ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-111", ViewerDidAuthor: true}, - {ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-222"}, - }}, + ID: "ISSUE-ID", + URL: "https://github.com/OWNER/REPO/issues/123", + Comments: comments, }, ghrepo.New("OWNER", "REPO"), nil } diff --git a/pkg/cmd/pr/comment/comment.go b/pkg/cmd/pr/comment/comment.go index 31a2bc25c65..a2ab4bf9ee0 100644 --- a/pkg/cmd/pr/comment/comment.go +++ b/pkg/cmd/pr/comment/comment.go @@ -10,12 +10,13 @@ import ( func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) error) *cobra.Command { opts := &shared.CommentableOptions{ - IO: f.IOStreams, - HttpClient: f.HttpClient, - EditSurvey: shared.CommentableEditSurvey(f.Config, f.IOStreams), - InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams), - ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey(f.Prompter), - OpenInBrowser: f.Browser.Browse, + IO: f.IOStreams, + HttpClient: f.HttpClient, + EditSurvey: shared.CommentableEditSurvey(f.Config, f.IOStreams), + InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams), + ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey(f.Prompter), + ConfirmCreateIfNoneSurvey: shared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter), + OpenInBrowser: f.Browser.Browse, } var bodyFile string @@ -75,6 +76,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in") cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment") cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the same author") + cmd.Flags().BoolVar(&opts.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last") return cmd } diff --git a/pkg/cmd/pr/comment/comment_test.go b/pkg/cmd/pr/comment/comment_test.go index 56cf58d2190..a6cc36abf64 100644 --- a/pkg/cmd/pr/comment/comment_test.go +++ b/pkg/cmd/pr/comment/comment_test.go @@ -129,6 +129,29 @@ func TestNewCmdComment(t *testing.T) { }, wantsErr: false, }, + { + name: "edit last flag", + input: "1 --edit-last", + output: shared.CommentableOptions{ + Interactive: true, + InputType: shared.InputTypeEditor, + Body: "", + EditLast: true, + }, + wantsErr: false, + }, + { + name: "edit last flag with create if none", + input: "1 --edit-last --create-if-none", + output: shared.CommentableOptions{ + Interactive: true, + InputType: shared.InputTypeEditor, + Body: "", + EditLast: true, + CreateIfNone: true, + }, + wantsErr: false, + }, { name: "body and body-file flags", input: "1 --body 'test' --body-file 'test-file.txt'", @@ -159,6 +182,12 @@ func TestNewCmdComment(t *testing.T) { output: shared.CommentableOptions{}, wantsErr: true, }, + { + name: "create-if-none flag without edit-last", + input: "1 --create-if-none", + output: shared.CommentableOptions{}, + wantsErr: true, + }, } for _, tt := range tests { @@ -208,11 +237,12 @@ func TestNewCmdComment(t *testing.T) { func Test_commentRun(t *testing.T) { tests := []struct { - name string - input *shared.CommentableOptions - httpStubs func(*testing.T, *httpmock.Registry) - stdout string - stderr string + name string + input *shared.CommentableOptions + emptyComments bool + httpStubs func(*testing.T, *httpmock.Registry) + stdout string + stderr string }{ { name: "interactive editor", @@ -245,6 +275,24 @@ func Test_commentRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n", }, + { + name: "interactive editor with edit last and create if none", + input: &shared.CommentableOptions{ + Interactive: true, + InputType: 0, + Body: "", + EditLast: true, + CreateIfNone: true, + + InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil }, + ConfirmCreateIfNoneSurvey: func() (bool, error) { return true, nil }, + ConfirmSubmitSurvey: func() (bool, error) { return true, nil }, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + mockCommentUpdate(t, reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n", + }, { name: "non-interactive web", input: &shared.CommentableOptions{ @@ -264,7 +312,43 @@ func Test_commentRun(t *testing.T) { Body: "", EditLast: true, - OpenInBrowser: func(string) error { return nil }, + OpenInBrowser: func(u string) error { + assert.Contains(t, u, "#issuecomment-111") + return nil + }, + }, + stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n", + }, + { + name: "non-interactive web with edit last and create if none for empty comments", + input: &shared.CommentableOptions{ + Interactive: false, + InputType: shared.InputTypeWeb, + Body: "", + EditLast: true, + CreateIfNone: true, + + OpenInBrowser: func(u string) error { + assert.Contains(t, u, "#issuecomment-new") + return nil + }, + }, + emptyComments: true, + stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n", + }, + { + name: "non-interactive web with edit last and create if none", + input: &shared.CommentableOptions{ + Interactive: false, + InputType: shared.InputTypeWeb, + Body: "", + EditLast: true, + CreateIfNone: true, + + OpenInBrowser: func(u string) error { + assert.Contains(t, u, "#issuecomment-111") + return nil + }, }, stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n", }, @@ -297,6 +381,23 @@ func Test_commentRun(t *testing.T) { }, stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n", }, + { + name: "non-interactive editor with edit last and create if none", + input: &shared.CommentableOptions{ + Interactive: false, + InputType: shared.InputTypeEditor, + Body: "", + EditLast: true, + CreateIfNone: true, + + EditSurvey: func(string) (string, error) { return "comment body", nil }, + }, + emptyComments: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + mockCommentCreate(t, reg) + }, + stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n", + }, { name: "non-interactive inline", input: &shared.CommentableOptions{ @@ -339,14 +440,20 @@ func Test_commentRun(t *testing.T) { tt.input.IO = ios tt.input.HttpClient = httpClient + + comments := api.Comments{Nodes: []api.Comment{ + {ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true}, + {ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-222"}, + }} + if tt.emptyComments { + comments.Nodes = []api.Comment{} + } + tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) { return &api.PullRequest{ - Number: 123, - URL: "https://github.com/OWNER/REPO/pull/123", - Comments: api.Comments{Nodes: []api.Comment{ - {ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true}, - {ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-222"}, - }}, + Number: 123, + URL: "https://github.com/OWNER/REPO/pull/123", + Comments: comments, }, ghrepo.New("OWNER", "REPO"), nil } diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 7a38286d2ee..a860d9ccb8c 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -17,6 +17,8 @@ import ( "github.com/spf13/cobra" ) +var ErrNoUserComments = errors.New("no comments found for current user") + type InputType int const ( @@ -32,19 +34,21 @@ type Commentable interface { } type CommentableOptions struct { - IO *iostreams.IOStreams - HttpClient func() (*http.Client, error) - RetrieveCommentable func() (Commentable, ghrepo.Interface, error) - EditSurvey func(string) (string, error) - InteractiveEditSurvey func(string) (string, error) - ConfirmSubmitSurvey func() (bool, error) - OpenInBrowser func(string) error - Interactive bool - InputType InputType - Body string - EditLast bool - Quiet bool - Host string + IO *iostreams.IOStreams + HttpClient func() (*http.Client, error) + RetrieveCommentable func() (Commentable, ghrepo.Interface, error) + EditSurvey func(string) (string, error) + InteractiveEditSurvey func(string) (string, error) + ConfirmSubmitSurvey func() (bool, error) + ConfirmCreateIfNoneSurvey func() (bool, error) + OpenInBrowser func(string) error + Interactive bool + InputType InputType + Body string + EditLast bool + CreateIfNone bool + Quiet bool + Host string } func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { @@ -66,6 +70,10 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { inputFlags++ } + if opts.CreateIfNone && !opts.EditLast { + return cmdutil.FlagErrorf("`--create-if-none` can only be used with `--edit-last`") + } + if inputFlags == 0 { if !opts.IO.CanPrompt() { return cmdutil.FlagErrorf("flags required when not running interactively") @@ -85,7 +93,24 @@ func CommentableRun(opts *CommentableOptions) error { } opts.Host = repo.RepoHost() if opts.EditLast { - return updateComment(commentable, opts) + err := updateComment(commentable, opts) + if !errors.Is(err, ErrNoUserComments) { + return err + } + + if opts.Interactive { + if opts.CreateIfNone { + fmt.Fprintln(opts.IO.ErrOut, "No comments found. Creating a new comment.") + } else { + cont, err := opts.ConfirmCreateIfNoneSurvey() + if err != nil { + return err + } + if !cont { + return ErrNoUserComments + } + } + } } return createComment(commentable, opts) } @@ -144,7 +169,7 @@ func createComment(commentable Commentable, opts *CommentableOptions) error { func updateComment(commentable Commentable, opts *CommentableOptions) error { comments := commentable.CurrentUserComments() if len(comments) == 0 { - return fmt.Errorf("no comments found for current user") + return ErrNoUserComments } lastComment := &comments[len(comments)-1] @@ -219,6 +244,12 @@ func CommentableInteractiveEditSurvey(cf func() (gh.Config, error), io *iostream } } +func CommentableInteractiveCreateIfNoneSurvey(p Prompt) func() (bool, error) { + return func() (bool, error) { + return p.Confirm("No comments found. Create one?", true) + } +} + func CommentableEditSurvey(cf func() (gh.Config, error), io *iostreams.IOStreams) func(string) (string, error) { return func(initialValue string) (string, error) { editorCommand, err := cmdutil.DetermineEditor(cf) From 026f64ce6fbff411f7c18308f0d9aac63dffacd0 Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 13 Feb 2025 10:41:53 +0500 Subject: [PATCH 0370/2338] Standardize URLs --- pkg/cmd/api/api.go | 4 ++-- pkg/cmd/extension/command.go | 2 +- pkg/cmd/repo/rename/rename.go | 4 ++-- pkg/cmd/root/help_topic.go | 2 +- pkg/cmd/search/code/code.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index a1308d09fdd..51febdfc917 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -201,12 +201,12 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command Annotations: map[string]string{ "help:environment": heredoc.Doc(` GH_TOKEN, GITHUB_TOKEN (in order of precedence): an authentication token for - github.com API requests. + API requests. GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN (in order of precedence): an authentication token for API requests to GitHub Enterprise. - GH_HOST: make the request to a GitHub host other than github.com. + GH_HOST: make the request to a GitHub host other than . `), }, Args: cobra.ExactArgs(1), diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 93c38537405..1c930fe5a78 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -300,7 +300,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { For GitHub repositories, the repository argument can be specified in %[1]sOWNER/REPO%[1]s format or as a full repository URL. - The URL format is useful when the repository is not hosted on github.com. + The URL format is useful when the repository is not hosted on . For remote repositories, the GitHub CLI first looks for the release artifacts assuming that it's a binary extension i.e. prebuilt binaries provided as part of the release. diff --git a/pkg/cmd/repo/rename/rename.go b/pkg/cmd/repo/rename/rename.go index e50095d7626..5656dd04628 100644 --- a/pkg/cmd/repo/rename/rename.go +++ b/pkg/cmd/repo/rename/rename.go @@ -59,7 +59,7 @@ func NewCmdRename(f *cmdutil.Factory, runf func(*RenameOptions) error) *cobra.Co with %[1]s--repo%[1]s is renamed. To transfer repository ownership to another user account or organization, - you must follow additional steps on GitHub.com + you must follow additional steps on . For more information on transferring repository ownership, see: @@ -126,7 +126,7 @@ func renameRun(opts *RenameOptions) error { } if strings.Contains(newRepoName, "/") { - return fmt.Errorf("New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see .") + return fmt.Errorf("New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on . For more information on transferring repository ownership, see .") } if opts.DoConfirm { diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 9ae2c5c8679..00e9cf552f0 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -43,7 +43,7 @@ var HelpTopics = []helpTopic{ short: "Environment variables that can be used with gh", long: heredoc.Docf(` %[1]sGH_TOKEN%[1]s, %[1]sGITHUB_TOKEN%[1]s (in order of precedence): an authentication token that will be used when - a command targets either github.com or a subdomain of ghe.com. Setting this avoids being prompted to + a command targets either or a subdomain of . Setting this avoids being prompted to authenticate and takes precedence over previously stored credentials. %[1]sGH_ENTERPRISE_TOKEN%[1]s, %[1]sGITHUB_ENTERPRISE_TOKEN%[1]s (in order of precedence): an authentication diff --git a/pkg/cmd/search/code/code.go b/pkg/cmd/search/code/code.go index 761d2602c86..bec29d3cfc6 100644 --- a/pkg/cmd/search/code/code.go +++ b/pkg/cmd/search/code/code.go @@ -40,7 +40,7 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman Note that these search results are powered by what is now a legacy GitHub code search engine. - The results might not match what is seen on github.com, and new features like regex search + The results might not match what is seen on , and new features like regex search are not yet available via the GitHub API. `), Example: heredoc.Doc(` From 8b7fb231ec1414cde5e8b8164fab14254f73d55f Mon Sep 17 00:00:00 2001 From: Azeem Sajid Date: Thu, 13 Feb 2025 11:06:27 +0500 Subject: [PATCH 0371/2338] Remove trailing whitespace --- pkg/cmd/actions/actions.go | 14 +++++++------- pkg/cmd/auth/login/login.go | 2 +- pkg/cmd/codespace/rebuild.go | 2 +- pkg/cmd/codespace/ssh.go | 6 +++--- pkg/cmd/codespace/view.go | 2 +- pkg/cmd/completion/completion.go | 6 +++--- pkg/cmd/gist/list/list_test.go | 10 +++++----- pkg/cmd/label/list_test.go | 2 +- pkg/cmd/pr/close/close.go | 2 +- pkg/cmd/pr/close/close_test.go | 2 +- pkg/cmd/project/item-edit/item_edit.go | 2 +- pkg/cmd/release/create/create.go | 2 +- pkg/cmd/release/view/view_test.go | 16 ++++++++-------- pkg/cmd/repo/delete/delete.go | 4 ++-- pkg/cmd/repo/deploy-key/add/add.go | 2 +- pkg/cmd/repo/gitignore/view/view.go | 2 +- pkg/cmd/repo/license/list/list.go | 2 +- pkg/cmd/repo/license/view/view_test.go | 2 +- pkg/cmd/repo/rename/rename.go | 6 +++--- pkg/cmd/ruleset/list/list.go | 2 +- pkg/cmd/ruleset/view/view.go | 2 +- pkg/cmd/ruleset/view/view_test.go | 12 ++++++------ pkg/cmd/run/rerun/rerun.go | 2 +- pkg/cmd/search/code/code.go | 4 ++-- 24 files changed, 54 insertions(+), 54 deletions(-) diff --git a/pkg/cmd/actions/actions.go b/pkg/cmd/actions/actions.go index 66386684433..76d72b9542d 100644 --- a/pkg/cmd/actions/actions.go +++ b/pkg/cmd/actions/actions.go @@ -34,19 +34,19 @@ func actionsExplainer(cs *iostreams.ColorScheme) string { GitHub CLI integrates with GitHub Actions to help you manage runs and workflows. %[3]s - gh run list: List recent workflow runs - gh run view: View details for a workflow run or one of its jobs - gh run watch: Watch a workflow run while it executes - gh run rerun: Rerun a failed workflow run + gh run list: List recent workflow runs + gh run view: View details for a workflow run or one of its jobs + gh run watch: Watch a workflow run while it executes + gh run rerun: Rerun a failed workflow run gh run download: Download artifacts generated by runs To see more help, run %[1]sgh help run %[1]s %[4]s gh workflow list: List workflow files in your repository - gh workflow view: View details for a workflow file - gh workflow enable: Enable a workflow file - gh workflow disable: Disable a workflow file + gh workflow view: View details for a workflow file + gh workflow enable: Enable a workflow file + gh workflow disable: Disable a workflow file gh workflow run: Trigger a workflow_dispatch run for a workflow file To see more help, run %[1]sgh help workflow %[1]s diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 347fcdb6a3c..95e32b7998d 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -72,7 +72,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s. Take care when passing a fine-grained personal access token to %[1]s--with-token%[1]s as the inherent scoping to certain resources may cause confusing behaviour when interacting with other - resources. Favour setting %[1]sGH_TOKEN$%[1]s for fine-grained personal access token usage. + resources. Favour setting %[1]sGH_TOKEN$%[1]s for fine-grained personal access token usage. Alternatively, gh will use the authentication token found in environment variables. This method is most suitable for "headless" use of gh such as in automation. See diff --git a/pkg/cmd/codespace/rebuild.go b/pkg/cmd/codespace/rebuild.go index 565406edc9c..8566c221139 100644 --- a/pkg/cmd/codespace/rebuild.go +++ b/pkg/cmd/codespace/rebuild.go @@ -23,7 +23,7 @@ func newRebuildCmd(app *App) *cobra.Command { Short: "Rebuild a codespace", Long: heredoc.Doc(` Rebuilding recreates your codespace. - + Your code and any current changes will be preserved. Your codespace will be rebuilt using your working directory's dev container. A full rebuild also removes cached Docker images. `), diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index b79ec68c920..70e7ceb420b 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -55,7 +55,7 @@ func newSSHCmd(app *App) *cobra.Command { Long: heredoc.Docf(` The %[1]sssh%[1]s command is used to SSH into a codespace. In its simplest form, you can run %[1]sgh cs ssh%[1]s, select a codespace interactively, and connect. - + The %[1]sssh%[1]s command will automatically create a public/private ssh key pair in the %[1]s~/.ssh%[1]s directory if you do not have an existing valid key pair. When selecting the key pair to use, the preferred order is: @@ -732,8 +732,8 @@ func newCpCmd(app *App) *cobra.Command { be evaluated on the remote machine, subject to expansion of tildes, braces, globs, environment variables, and backticks. For security, do not use this flag with arguments provided by untrusted users; see for discussion. - - By default, the %[1]scp%[1]s command will create a public/private ssh key pair to authenticate with + + By default, the %[1]scp%[1]s command will create a public/private ssh key pair to authenticate with the codespace inside the %[1]s~/.ssh directory%[1]s. `, "`"), Example: heredoc.Doc(` diff --git a/pkg/cmd/codespace/view.go b/pkg/cmd/codespace/view.go index e212f08203a..205d5467355 100644 --- a/pkg/cmd/codespace/view.go +++ b/pkg/cmd/codespace/view.go @@ -30,7 +30,7 @@ func newViewCmd(app *App) *cobra.Command { Short: "View details about a codespace", Example: heredoc.Doc(` # select a codespace from a list of all codespaces you own - $ gh cs view + $ gh cs view # view the details of a specific codespace $ gh cs view -c codespace-name-12345 diff --git a/pkg/cmd/completion/completion.go b/pkg/cmd/completion/completion.go index d83dd31ef60..b34bf7abfb5 100644 --- a/pkg/cmd/completion/completion.go +++ b/pkg/cmd/completion/completion.go @@ -33,7 +33,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { After, add this to your %[1]s~/.bash_profile%[1]s: eval "$(gh completion -s bash)" - + ### zsh Generate a %[1]s_gh%[1]s completion script and put it somewhere in your %[1]s$fpath%[1]s: @@ -44,7 +44,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { autoload -U compinit compinit -i - + Zsh version 5.7 or later is recommended. ### fish @@ -59,7 +59,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { mkdir -Path (Split-Path -Parent $profile) -ErrorAction SilentlyContinue notepad $profile - + Add the line and save the file: Invoke-Expression -Command $(gh completion -s powershell | Out-String) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index e6cb35d53de..0acfae109d5 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -540,14 +540,14 @@ func Test_listRun(t *testing.T) { wantOut: heredoc.Doc(` 1234 main.txt octo match in the description - + 2345 octo.txt match in the file name 3456 main.txt match in the file text octo in the text - + `), }, { @@ -599,14 +599,14 @@ func Test_listRun(t *testing.T) { wantOut: heredoc.Docf(` %[1]s[0;34m1234%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m - + %[1]s[0;34m2345%[1]s[0m %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;32m.txt%[1]s[0m %[1]s[0;1;39mmatch in the file name%[1]s[0m - + %[1]s[0;34m3456%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m %[1]s[0;1;39mmatch in the file text%[1]s[0m %[1]s[0;30;43mocto%[1]s[0m in the text - + `, "\x1b"), }, } diff --git a/pkg/cmd/label/list_test.go b/pkg/cmd/label/list_test.go index 6f066da32cf..8a1bc7c7ac0 100644 --- a/pkg/cmd/label/list_test.go +++ b/pkg/cmd/label/list_test.go @@ -218,7 +218,7 @@ func TestListRun(t *testing.T) { wantStdout: heredoc.Doc(` Showing 2 of 2 labels in OWNER/REPO - + NAME DESCRIPTION COLOR bug This is a bug label #d73a4a docs This is a docs label #ffa8da diff --git a/pkg/cmd/pr/close/close.go b/pkg/cmd/pr/close/close.go index ec51323772e..063501ef7ed 100644 --- a/pkg/cmd/pr/close/close.go +++ b/pkg/cmd/pr/close/close.go @@ -143,7 +143,7 @@ func closeRun(opts *CloseOptions) error { branchSwitchString = fmt.Sprintf(" and switched to branch %s", cs.Cyan(branchToSwitchTo)) } } else { - fmt.Fprintf(opts.IO.ErrOut, "%s Skipped deleting the local branch since current directory is not a git repository \n", cs.WarningIcon()) + fmt.Fprintf(opts.IO.ErrOut, "%s Skipped deleting the local branch since current directory is not a git repository\n", cs.WarningIcon()) } } diff --git a/pkg/cmd/pr/close/close_test.go b/pkg/cmd/pr/close/close_test.go index f957581f84a..959af0e04f2 100644 --- a/pkg/cmd/pr/close/close_test.go +++ b/pkg/cmd/pr/close/close_test.go @@ -271,7 +271,7 @@ func TestPrClose_deleteBranch_notInGitRepo(t *testing.T) { assert.Equal(t, "", output.String()) assert.Equal(t, heredoc.Doc(` ✓ Closed pull request OWNER/REPO#96 (The title of the PR) - ! Skipped deleting the local branch since current directory is not a git repository + ! Skipped deleting the local branch since current directory is not a git repository ✓ Deleted branch trunk `), output.Stderr()) } diff --git a/pkg/cmd/project/item-edit/item_edit.go b/pkg/cmd/project/item-edit/item_edit.go index 657af1f54ac..c4acaaea9ef 100644 --- a/pkg/cmd/project/item-edit/item_edit.go +++ b/pkg/cmd/project/item-edit/item_edit.go @@ -63,7 +63,7 @@ func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error) Short: "Edit an item in a project", Long: heredoc.Docf(` Edit either a draft issue or a project item. Both usages require the ID of the item to edit. - + For non-draft issues, the ID of the project is also required, and only a single field value can be updated per invocation. Remove project item field value using %[1]s--clear%[1]s flag. diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 8e77546d0a7..6da461a88c3 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -120,7 +120,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co $ gh release create v1.2.3 --notes-from-tag Don't mark the release as latest - $ gh release create v1.2.3 --latest=false + $ gh release create v1.2.3 --latest=false Upload all tarballs in a directory as release assets $ gh release create v1.2.3 ./dist/*.tgz diff --git a/pkg/cmd/release/view/view_test.go b/pkg/cmd/release/view/view_test.go index 6fc747f69c9..1fed0fdc8e4 100644 --- a/pkg/cmd/release/view/view_test.go +++ b/pkg/cmd/release/view/view_test.go @@ -144,15 +144,15 @@ func Test_viewRun(t *testing.T) { wantStdout: heredoc.Doc(` v1.2.3 MonaLisa released this about 1 day ago - + • Fixed bugs - - + + Assets windows.zip 12 B linux.tgz 34 B - + View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3 `), wantStderr: ``, @@ -168,15 +168,15 @@ func Test_viewRun(t *testing.T) { wantStdout: heredoc.Doc(` v1.2.3 MonaLisa released this about 1 day ago - + • Fixed bugs - - + + Assets windows.zip 12 B linux.tgz 34 B - + View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3 `), wantStderr: ``, diff --git a/pkg/cmd/repo/delete/delete.go b/pkg/cmd/repo/delete/delete.go index 7c6476f1d25..50df0ddac8e 100644 --- a/pkg/cmd/repo/delete/delete.go +++ b/pkg/cmd/repo/delete/delete.go @@ -41,10 +41,10 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co Short: "Delete a repository", Long: heredoc.Docf(` Delete a GitHub repository. - + With no argument, deletes the current repository. Otherwise, deletes the specified repository. - Deletion requires authorization with the %[1]sdelete_repo%[1]s scope. + Deletion requires authorization with the %[1]sdelete_repo%[1]s scope. To authorize, run %[1]sgh auth refresh -s delete_repo%[1]s `, "`"), Args: cobra.MaximumNArgs(1), diff --git a/pkg/cmd/repo/deploy-key/add/add.go b/pkg/cmd/repo/deploy-key/add/add.go index 473d42498c9..dbac7dcfb1d 100644 --- a/pkg/cmd/repo/deploy-key/add/add.go +++ b/pkg/cmd/repo/deploy-key/add/add.go @@ -34,7 +34,7 @@ func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command Short: "Add a deploy key to a GitHub repository", Long: heredoc.Doc(` Add a deploy key to a GitHub repository. - + Note that any key added by gh will be associated with the current authentication token. If you de-authorize the GitHub CLI app or authentication token from your account, any deploy keys added by GitHub CLI will be removed as well. diff --git a/pkg/cmd/repo/gitignore/view/view.go b/pkg/cmd/repo/gitignore/view/view.go index 46e674632ff..b22f94ed9a0 100644 --- a/pkg/cmd/repo/gitignore/view/view.go +++ b/pkg/cmd/repo/gitignore/view/view.go @@ -34,7 +34,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman Short: "View an available repository gitignore template", Long: heredoc.Docf(` View an available repository %[1]s.gitignore%[1]s template. - + %[1]s