From ff6c7b925f99250b33618ad6249cc75177c76001 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 10 Feb 2022 13:53:55 -0800 Subject: [PATCH 01/75] Add flag to rerun only failed jobs in a workflow run --- pkg/cmd/run/rerun/rerun.go | 19 +++++++++++++++--- pkg/cmd/run/rerun/rerun_test.go | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 9788a3061..98cfa7060 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -18,7 +18,8 @@ type RerunOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) - RunID string + RunID string + OnlyFailed bool Prompt bool } @@ -52,6 +53,8 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm }, } + cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + return cmd } @@ -98,7 +101,12 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to get run: %w", err) } - path := fmt.Sprintf("repos/%s/actions/runs/%d/rerun", ghrepo.FullName(repo), run.ID) + runVerb := "rerun" + if opts.OnlyFailed { + runVerb = "rerun-failed-jobs" + } + + path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb) err = client.REST(repo.RepoHost(), "POST", path, nil, nil) if err != nil { @@ -111,8 +119,13 @@ func runRerun(opts *RerunOptions) error { if opts.IO.CanPrompt() { cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Requested rerun of run %s\n", + onlyFailedMsg := "" + if opts.OnlyFailed { + onlyFailedMsg = "(failed jobs) " + } + fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n", cs.SuccessIcon(), + onlyFailedMsg, cs.Cyanf("%d", run.ID)) } diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 11362ce4b..3fcf68ff3 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -50,6 +50,23 @@ func TestNewCmdRerun(t *testing.T) { RunID: "1234", }, }, + { + name: "failed arg nontty", + cli: "4321 --failed", + wants: RerunOptions{ + RunID: "4321", + OnlyFailed: true, + }, + }, + { + name: "failed arg", + tty: true, + cli: "--failed", + wants: RerunOptions{ + Prompt: true, + OnlyFailed: true, + }, + }, } for _, tt := range tests { @@ -117,6 +134,23 @@ func TestRerun(t *testing.T) { }, wantOut: "✓ Requested rerun of run 1234\n", }, + { + name: "arg including onlyFailed", + tty: true, + opts: &RerunOptions{ + RunID: "1234", + OnlyFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), + httpmock.StringResponse("{}")) + }, + wantOut: "✓ Requested rerun (failed jobs) of run 1234\n", + }, { name: "prompt", tty: true, From 04a4e43decff2650745bc8651233f4983667bbfd Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 14 Feb 2022 17:22:58 -0500 Subject: [PATCH 02/75] Initial spike with request/event handling --- pkg/cmd/codespace/ports.go | 53 +++++++++++++++++++++++++++++++ pkg/liveshare/port_forwarder.go | 3 ++ pkg/liveshare/rpc.go | 48 +++++++++++++++++++++++++--- pkg/liveshare/rpc_test.go | 55 +++++++++++++++++++++++++++++++++ pkg/liveshare/session.go | 4 +++ 5 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 pkg/liveshare/rpc_test.go diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 094833e30..4f35b33ce 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -7,8 +7,10 @@ import ( "errors" "fmt" "net" + "net/http" "strconv" "strings" + "time" "github.com/cli/cli/v2/internal/codespaces" "github.com/cli/cli/v2/internal/codespaces/api" @@ -253,6 +255,15 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar for _, port := range ports { a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility)) err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) + + // wait for succeed or failure + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + if err := a.waitForPortUpdate(ctx, session, port.number); err != nil { + return fmt.Errorf("error waiting for port update: %w", err) + } + a.StopProgressIndicator() if err != nil { return fmt.Errorf("error update port to public: %w", err) @@ -262,6 +273,48 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return nil } +type portChangeKind string + +const ( + portChangeKindUpdate portChangeKind = "update" +) + +type portData struct { + Port int `json:"port"` + ChangeKind portChangeKind `json:"changeKind"` + ErrorDetail string `json:"errorDetail"` + StatusCode int `json:"statusCode"` +} + +func (a *App) waitForPortUpdate(ctx context.Context, session *liveshare.Session, port int) error { + success := session.WaitForEvent("sharingSucceeded") + failure := session.WaitForEvent("sharingFailed") + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for server sharing to succeed or fail") + case b := <-success: + if err := json.Unmarshal(b, &portData); err != nil { + return fmt.Errorf("error unmarshaling port data: %w", err) + } + if portData.Port == port && portData.ChangeKind == portChangeKindUpdate { + return nil + } + case b := <-failure: + if err := json.Unmarshal(b, &portData); err != nil { + return fmt.Errorf("error unmarshaling port data: %w", err) + } + if portData.Port == port && portData.ChangeKind == portChangeKindUpdate { + if portData.StatusCode == http.StatusForbidden { + return errors.New("organization admin has forbidden this privacy setting") + } + return errors.New(portData.ErrorDetail) + } + } + } +} + type portVisibility struct { number int visibility string diff --git a/pkg/liveshare/port_forwarder.go b/pkg/liveshare/port_forwarder.go index 2649abd3c..546201f12 100644 --- a/pkg/liveshare/port_forwarder.go +++ b/pkg/liveshare/port_forwarder.go @@ -97,6 +97,9 @@ func (fwd *PortForwarder) shareRemotePort(ctx context.Context) (channelID, error if err != nil { err = fmt.Errorf("failed to share remote port %d: %w", fwd.remotePort, err) } + + // wait for port change kind start + return id, err } diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 4ab8fbb88..8c411f1ff 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "sync" "time" "github.com/opentracing/opentracing-go" @@ -13,15 +14,18 @@ import ( type rpcClient struct { *jsonrpc2.Conn conn io.ReadWriteCloser + + eventHandlersMu sync.RWMutex + eventHandlers map[string]chan []byte } func newRPCClient(conn io.ReadWriteCloser) *rpcClient { - return &rpcClient{conn: conn} + return &rpcClient{conn: conn, eventHandlers: make(map[string]chan []byte)} } func (r *rpcClient) connect(ctx context.Context) { stream := jsonrpc2.NewBufferedStream(r.conn, jsonrpc2.VSCodeObjectCodec{}) - r.Conn = jsonrpc2.NewConn(ctx, stream, nullHandler{}) + r.Conn = jsonrpc2.NewConn(ctx, stream, newRequestHandler(r)) } func (r *rpcClient) do(ctx context.Context, method string, args, result interface{}) error { @@ -40,7 +44,43 @@ func (r *rpcClient) do(ctx context.Context, method string, args, result interfac return waiter.Wait(waitCtx, result) } -type nullHandler struct{} +func (r *rpcClient) registerEventHandler(eventName string) chan []byte { + r.eventHandlersMu.Lock() + defer r.eventHandlersMu.Unlock() -func (nullHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + if ch, ok := r.eventHandlers[eventName]; ok { + return ch + } + + ch := make(chan []byte) + r.eventHandlers[eventName] = ch + return ch +} + +func (r *rpcClient) eventHandler(eventName string) chan []byte { + r.eventHandlersMu.RLock() + defer r.eventHandlersMu.RUnlock() + + return r.eventHandlers[eventName] +} + +type requestHandler struct { + rpcClient *rpcClient +} + +func newRequestHandler(rpcClient *rpcClient) *requestHandler { + return &requestHandler{rpcClient: rpcClient} +} + +func (e *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + handler := e.rpcClient.eventHandler(req.Method) + if handler == nil { + return // noop + } + + select { + case handler <- *req.Params: + default: + // event handler + } } diff --git a/pkg/liveshare/rpc_test.go b/pkg/liveshare/rpc_test.go new file mode 100644 index 000000000..0f78b8780 --- /dev/null +++ b/pkg/liveshare/rpc_test.go @@ -0,0 +1,55 @@ +package liveshare + +import ( + "context" + "encoding/json" + "fmt" + "net" + "testing" + + "github.com/sourcegraph/jsonrpc2" +) + +func TestRequestHandler(t *testing.T) { + r, w := net.Pipe() + client := newRPCClient(r) + + ctx := context.Background() + client.connect(ctx) + + type params struct { + Data string `json:"data"` + } + + ev := client.registerEventHandler("testEvent") + done := make(chan error) + go func() { + b := <-ev + var receivedParams params + if err := json.Unmarshal(b, &receivedParams); err != nil { + done <- err + return + } + if receivedParams.Data != "test" { + done <- fmt.Errorf("expected test, got %q", receivedParams.Data) + } + done <- nil + }() + + go func() { + codec := jsonrpc2.VSCodeObjectCodec{} + type message struct { + Method string `json:"method"` + Params params `json:"params"` + } + codec.WriteObject(w, message{ + Method: "testEvent", + Params: params{"test"}, + }) + }() + + err := <-done + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index b4bc3c16f..715d24fc6 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -78,6 +78,10 @@ func (s *Session) UpdateSharedServerPrivacy(ctx context.Context, port int, visib return nil } +func (s *Session) WaitForEvent(eventName string) chan []byte { + return s.rpc.registerEventHandler(eventName) +} + // StartsSSHServer starts an SSH server in the container, installing sshd if necessary, // and returns the port on which it listens and the user name clients should provide. func (s *Session) StartSSHServer(ctx context.Context) (int, string, error) { From c90da9799d687890af5636e4563d288672330e93 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 15 Feb 2022 15:30:06 -0500 Subject: [PATCH 03/75] Tests for update port visibility --- pkg/cmd/codespace/ports.go | 14 +++-- pkg/cmd/codespace/ports_test.go | 97 +++++++++++++++++++++++++++++++++ pkg/liveshare/client.go | 8 ++- pkg/liveshare/test/server.go | 26 ++++++++- 4 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 pkg/cmd/codespace/ports_test.go diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 4f35b33ce..02c70ada6 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -260,6 +260,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() + fmt.Println("watiing for update") if err := a.waitForPortUpdate(ctx, session, port.number); err != nil { return fmt.Errorf("error waiting for port update: %w", err) } @@ -291,25 +292,26 @@ func (a *App) waitForPortUpdate(ctx context.Context, session *liveshare.Session, failure := session.WaitForEvent("sharingFailed") for { + var pd portData select { case <-ctx.Done(): return fmt.Errorf("timeout waiting for server sharing to succeed or fail") case b := <-success: - if err := json.Unmarshal(b, &portData); err != nil { + if err := json.Unmarshal(b, &pd); err != nil { return fmt.Errorf("error unmarshaling port data: %w", err) } - if portData.Port == port && portData.ChangeKind == portChangeKindUpdate { + if pd.Port == port && pd.ChangeKind == portChangeKindUpdate { return nil } case b := <-failure: - if err := json.Unmarshal(b, &portData); err != nil { + if err := json.Unmarshal(b, &pd); err != nil { return fmt.Errorf("error unmarshaling port data: %w", err) } - if portData.Port == port && portData.ChangeKind == portChangeKindUpdate { - if portData.StatusCode == http.StatusForbidden { + if pd.Port == port && pd.ChangeKind == portChangeKindUpdate { + if pd.StatusCode == http.StatusForbidden { return errors.New("organization admin has forbidden this privacy setting") } - return errors.New(portData.ErrorDetail) + return errors.New(pd.ErrorDetail) } } } diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go new file mode 100644 index 000000000..5c2de260c --- /dev/null +++ b/pkg/cmd/codespace/ports_test.go @@ -0,0 +1,97 @@ +package codespace + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" + livesharetest "github.com/cli/cli/v2/pkg/liveshare/test" + "github.com/sourcegraph/jsonrpc2" +) + +type joinWorkspaceResult struct { + SessionNumber int `json:"sessionNumber"` +} + +func TestPortsUpdateVisibility(t *testing.T) { + joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { + return joinWorkspaceResult{1}, nil + } + const sessionToken = "session-token" + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ch := make(chan float64, 1) + updateSharedVisibility := func(rpcReq *jsonrpc2.Request) (interface{}, error) { + var req []interface{} + if err := json.Unmarshal(*rpcReq.Params, &req); err != nil { + return nil, fmt.Errorf("unmarshal req: %w", err) + } + + ch <- req[0].(float64) + return nil, nil + } + testServer, err := livesharetest.NewServer( + livesharetest.WithNonSecure(), + livesharetest.WithPassword(sessionToken), + livesharetest.WithService("workspace.joinWorkspace", joinWorkspace), + livesharetest.WithService("serverSharing.updateSharedServerPrivacy", updateSharedVisibility), + ) + if err != nil { + t.Fatal(err) + } + + type rpcMessage struct { + Method string + Params portData + } + + go func() { + for { + select { + case <-ctx.Done(): + return + case port := <-ch: + testServer.WriteToObjectStream(rpcMessage{ + Method: "sharingSucceeded", + Params: portData{ + Port: int(port), + ChangeKind: portChangeKindUpdate, + }, + }) + } + } + }() + + mockApi := &apiClientMock{ + GetCodespaceFunc: func(ctx context.Context, codespaceName string, includeConnection bool) (*api.Codespace, error) { + return &api.Codespace{ + Name: "codespace-name", + State: api.CodespaceStateAvailable, + Connection: api.CodespaceConnection{ + SessionID: "session-id", + SessionToken: sessionToken, + RelayEndpoint: testServer.URL(), + RelaySAS: "relay-sas", + HostPublicKeys: []string{livesharetest.SSHPublicKey}, + }, + }, nil + }, + } + + fmt.Println(testServer) + io, _, _, _ := iostreams.Test() + a := &App{ + io: io, + apiClient: mockApi, + } + + err = a.UpdatePortVisibility(ctx, "codespace-name", []string{"80:80", "9999:9999"}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/pkg/liveshare/client.go b/pkg/liveshare/client.go index 9427ddf05..b67e1b1cf 100644 --- a/pkg/liveshare/client.go +++ b/pkg/liveshare/client.go @@ -57,7 +57,13 @@ func (opts *Options) uri(action string) (string, error) { sas := url.QueryEscape(opts.RelaySAS) uri := opts.RelayEndpoint - uri = strings.Replace(uri, "sb:", "wss:", -1) + + if strings.HasPrefix(uri, "http:") { + uri = strings.Replace(uri, "http:", "ws:", 1) + } else { + uri = strings.Replace(uri, "sb:", "wss:", -1) + } + uri = strings.Replace(uri, ".net/", ".net:443/$hc/", 1) uri = uri + "?sb-hc-action=" + action + "&sb-hc-token=" + sas return uri, nil diff --git a/pkg/liveshare/test/server.go b/pkg/liveshare/test/server.go index 3f038a15b..7f5340db4 100644 --- a/pkg/liveshare/test/server.go +++ b/pkg/liveshare/test/server.go @@ -42,6 +42,9 @@ type Server struct { sshConfig *ssh.ServerConfig httptestServer *httptest.Server errCh chan error + nonSecure bool + + objectStream jsonrpc2.ObjectStream } // NewServer creates a new Server. ServerOptions can be passed to configure @@ -65,7 +68,12 @@ func NewServer(opts ...ServerOption) (*Server, error) { server.sshConfig.AddHostKey(privateKey) server.errCh = make(chan error, 1) - server.httptestServer = httptest.NewTLSServer(http.HandlerFunc(makeConnection(server))) + + if server.nonSecure { + server.httptestServer = httptest.NewServer(http.HandlerFunc(makeConnection(server))) + } else { + server.httptestServer = httptest.NewTLSServer(http.HandlerFunc(makeConnection(server))) + } return server, nil } @@ -80,6 +88,13 @@ func WithPassword(password string) ServerOption { } } +func WithNonSecure() ServerOption { + return func(s *Server) error { + s.nonSecure = true + return nil + } +} + // WithService accepts a mock RPC service for the Server to invoke. func WithService(serviceName string, handler RPCHandleFunc) ServerOption { return func(s *Server) error { @@ -134,6 +149,13 @@ func (s *Server) Err() <-chan error { return s.errCh } +func (s *Server) WriteToObjectStream(obj interface{}) error { + if s.objectStream == nil { + return errors.New("object stream not set") + } + return s.objectStream.WriteObject(obj) +} + var upgrader = websocket.Upgrader{} func makeConnection(server *Server) http.HandlerFunc { @@ -300,6 +322,8 @@ func forwardStream(ctx context.Context, server *Server, streamName string, chann func handleChannel(server *Server, channel ssh.Channel) { stream := jsonrpc2.NewBufferedStream(channel, jsonrpc2.VSCodeObjectCodec{}) + server.objectStream = stream + jsonrpc2.NewConn(context.Background(), stream, newRPCHandler(server)) } From c96807fae7a99a32f47adc0094ba3a1a6ae14ca5 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 15 Feb 2022 15:34:04 -0500 Subject: [PATCH 04/75] Clean up cruft --- pkg/cmd/codespace/ports.go | 1 - pkg/cmd/codespace/ports_test.go | 1 - pkg/liveshare/rpc.go | 6 +----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 02c70ada6..2e881c1d8 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -260,7 +260,6 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - fmt.Println("watiing for update") if err := a.waitForPortUpdate(ctx, session, port.number); err != nil { return fmt.Errorf("error waiting for port update: %w", err) } diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index 5c2de260c..c6f2f88f7 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -83,7 +83,6 @@ func TestPortsUpdateVisibility(t *testing.T) { }, } - fmt.Println(testServer) io, _, _, _ := iostreams.Test() a := &App{ io: io, diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 8c411f1ff..5187dc8de 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -78,9 +78,5 @@ func (e *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *j return // noop } - select { - case handler <- *req.Params: - default: - // event handler - } + handler <- *req.Params } From ca14d10b7bed0565b038a2a01d30df2524750181 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Wed, 16 Feb 2022 03:10:47 +0000 Subject: [PATCH 05/75] add failure tests --- pkg/cmd/codespace/ports.go | 11 ++- pkg/cmd/codespace/ports_test.go | 157 ++++++++++++++++++++++++++++---- 2 files changed, 144 insertions(+), 24 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 2e881c1d8..737e824af 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -256,18 +256,19 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility)) err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) - // wait for succeed or failure + if err != nil { + return fmt.Errorf("error updating port %d to %s: %w", port.number, port.visibility, err) + } + + // wait for success or failure ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() if err := a.waitForPortUpdate(ctx, session, port.number); err != nil { - return fmt.Errorf("error waiting for port update: %w", err) + return fmt.Errorf("error waiting for port %d to update to %s: %w", port.number, port.visibility, err) } a.StopProgressIndicator() - if err != nil { - return fmt.Errorf("error update port to public: %w", err) - } } return nil diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index c6f2f88f7..a936223d5 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -12,11 +12,127 @@ import ( "github.com/sourcegraph/jsonrpc2" ) +func TestPortsUpdateVisibilitySuccess(t *testing.T) { + portVisibilities := []portVisibility{ + { + number: 80, + visibility: "org", + }, + { + number: 9999, + visibility: "public", + }, + } + + eventResponses := []string{ + "sharingSucceeded", + "sharingSucceeded", + } + + portsData := []portData{ + { + Port: 80, + ChangeKind: portChangeKindUpdate, + }, + { + Port: 9999, + ChangeKind: portChangeKindUpdate, + }, + } + + err := RunUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +func TestPortsUpdateVisibilityFailure403(t *testing.T) { + portVisibilities := []portVisibility{ + { + number: 80, + visibility: "org", + }, + { + number: 9999, + visibility: "public", + }, + } + + eventResponses := []string{ + "sharingSucceeded", + "sharingFailed", + } + + portsData := []portData{ + { + Port: 80, + ChangeKind: portChangeKindUpdate, + }, + { + Port: 9999, + ChangeKind: portChangeKindUpdate, + ErrorDetail: "test error", + StatusCode: 403, + }, + } + + err := RunUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) + if err == nil { + t.Errorf("unexpected error: %v", err) + } + + expectedErr := "error waiting for port 9999 to update to public: organization admin has forbidden this privacy setting" + if err.Error() != expectedErr { + t.Errorf("expected: %v, got: %v", expectedErr, err) + } +} + +func TestPortsUpdateVisibilityFailure(t *testing.T) { + portVisibilities := []portVisibility{ + { + number: 80, + visibility: "org", + }, + { + number: 9999, + visibility: "public", + }, + } + + eventResponses := []string{ + "sharingSucceeded", + "sharingFailed", + } + + portsData := []portData{ + { + Port: 80, + ChangeKind: portChangeKindUpdate, + }, + { + Port: 9999, + ChangeKind: portChangeKindUpdate, + ErrorDetail: "test error", + }, + } + + err := RunUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) + if err == nil { + t.Errorf("unexpected error: %v", err) + } + + expectedErr := "error waiting for port 9999 to update to public: test error" + if err.Error() != expectedErr { + t.Errorf("expected: %v, got: %v", expectedErr, err) + } +} + type joinWorkspaceResult struct { SessionNumber int `json:"sessionNumber"` } -func TestPortsUpdateVisibility(t *testing.T) { +func RunUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, eventResponses []string, portsData []portData) error { joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } @@ -50,22 +166,21 @@ func TestPortsUpdateVisibility(t *testing.T) { Params portData } - go func() { - for { - select { - case <-ctx.Done(): - return - case port := <-ch: - testServer.WriteToObjectStream(rpcMessage{ - Method: "sharingSucceeded", - Params: portData{ - Port: int(port), - ChangeKind: portChangeKindUpdate, - }, - }) + for index, pd := range portsData { + go func(index int, pd portData) { + for { + select { + case <-ctx.Done(): + return + case <-ch: + testServer.WriteToObjectStream(rpcMessage{ + Method: eventResponses[index], + Params: pd, + }) + } } - } - }() + }(index, pd) + } mockApi := &apiClientMock{ GetCodespaceFunc: func(ctx context.Context, codespaceName string, includeConnection bool) (*api.Codespace, error) { @@ -89,8 +204,12 @@ func TestPortsUpdateVisibility(t *testing.T) { apiClient: mockApi, } - err = a.UpdatePortVisibility(ctx, "codespace-name", []string{"80:80", "9999:9999"}) - if err != nil { - t.Errorf("unexpected error: %v", err) + var portArgs []string + for _, pv := range portVisibilities { + portArgs = append(portArgs, fmt.Sprintf("%d:%s", pv.number, pv.visibility)) } + + err = a.UpdatePortVisibility(ctx, "codespace-name", portArgs) + + return err } From ee349700de342b6be71ad95d79baf03b17a65da5 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Wed, 16 Feb 2022 17:56:09 -0500 Subject: [PATCH 06/75] Checkin almost final impl but wrong direction --- pkg/cmd/codespace/ports.go | 36 ++++++++++++++++++++---- pkg/cmd/codespace/ports_test.go | 50 ++++++++++++++++++--------------- pkg/liveshare/rpc.go | 39 +++++++++++++------------ pkg/liveshare/session.go | 4 +-- pkg/liveshare/ssh.go | 10 +++++++ 5 files changed, 90 insertions(+), 49 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 737e824af..3b698494a 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -231,6 +231,28 @@ func newPortsVisibilityCmd(app *App) *cobra.Command { } } +type ErrUpdatingPortVisibility struct { + port int + visibility string + err error +} + +func newErrUpdatingPortVisibility(port int, visibility string, err error) *ErrUpdatingPortVisibility { + return &ErrUpdatingPortVisibility{ + port: port, + visibility: visibility, + err: err, + } +} + +func (e *ErrUpdatingPortVisibility) Error() string { + return fmt.Sprintf("error waiting for port %d to update to %s: %s", e.port, e.visibility, e.err) +} + +func (e *ErrUpdatingPortVisibility) Unwrap() error { + return e.err +} + func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, args []string) (err error) { ports, err := a.parsePortVisibilities(args) if err != nil { @@ -251,6 +273,9 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar } defer safeClose(session, &err) + success := session.RegisterEvent("sharingSucceeded") + failure := session.RegisterEvent("sharingFailed") + // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility)) @@ -264,8 +289,8 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - if err := a.waitForPortUpdate(ctx, session, port.number); err != nil { - return fmt.Errorf("error waiting for port %d to update to %s: %w", port.number, port.visibility, err) + if err := a.waitForPortUpdate(ctx, success, failure, session, port.number); err != nil { + return newErrUpdatingPortVisibility(port.number, port.visibility, err) } a.StopProgressIndicator() @@ -287,10 +312,9 @@ type portData struct { StatusCode int `json:"statusCode"` } -func (a *App) waitForPortUpdate(ctx context.Context, session *liveshare.Session, port int) error { - success := session.WaitForEvent("sharingSucceeded") - failure := session.WaitForEvent("sharingFailed") +var errUpdatePortVisibilityForbidden = errors.New("organization admin has forbidden this privacy setting") +func (a *App) waitForPortUpdate(ctx context.Context, success, failure chan []byte, session *liveshare.Session, port int) error { for { var pd portData select { @@ -309,7 +333,7 @@ func (a *App) waitForPortUpdate(ctx context.Context, session *liveshare.Session, } if pd.Port == port && pd.ChangeKind == portChangeKindUpdate { if pd.StatusCode == http.StatusForbidden { - return errors.New("organization admin has forbidden this privacy setting") + return errUpdatePortVisibilityForbidden } return errors.New(pd.ErrorDetail) } diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index a936223d5..9f4a3f839 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -3,6 +3,7 @@ package codespace import ( "context" "encoding/json" + "errors" "fmt" "testing" @@ -40,7 +41,7 @@ func TestPortsUpdateVisibilitySuccess(t *testing.T) { }, } - err := RunUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) + err := runUpdateVisibilityTest(portVisibilities, eventResponses, portsData) if err != nil { t.Errorf("unexpected error: %v", err) @@ -77,14 +78,13 @@ func TestPortsUpdateVisibilityFailure403(t *testing.T) { }, } - err := RunUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) + err := runUpdateVisibilityTest(portVisibilities, eventResponses, portsData) if err == nil { t.Errorf("unexpected error: %v", err) } - expectedErr := "error waiting for port 9999 to update to public: organization admin has forbidden this privacy setting" - if err.Error() != expectedErr { - t.Errorf("expected: %v, got: %v", expectedErr, err) + if errors.Unwrap(err) != errUpdatePortVisibilityForbidden { + t.Errorf("expected: %v, got: %v", errUpdatePortVisibilityForbidden, errors.Unwrap(err)) } } @@ -117,13 +117,13 @@ func TestPortsUpdateVisibilityFailure(t *testing.T) { }, } - err := RunUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) + err := runUpdateVisibilityTest(portVisibilities, eventResponses, portsData) if err == nil { t.Errorf("unexpected error: %v", err) } - expectedErr := "error waiting for port 9999 to update to public: test error" - if err.Error() != expectedErr { + var expectedErr *ErrUpdatingPortVisibility + if !errors.As(err, &expectedErr) { t.Errorf("expected: %v, got: %v", expectedErr, err) } } @@ -132,7 +132,7 @@ type joinWorkspaceResult struct { SessionNumber int `json:"sessionNumber"` } -func RunUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, eventResponses []string, portsData []portData) error { +func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses []string, portsData []portData) error { joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } @@ -158,7 +158,7 @@ func RunUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev livesharetest.WithService("serverSharing.updateSharedServerPrivacy", updateSharedVisibility), ) if err != nil { - t.Fatal(err) + return fmt.Errorf("unable to create test server: %w", err) } type rpcMessage struct { @@ -166,21 +166,25 @@ func RunUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev Params portData } - for index, pd := range portsData { - go func(index int, pd portData) { - for { - select { - case <-ctx.Done(): - return - case <-ch: - testServer.WriteToObjectStream(rpcMessage{ - Method: eventResponses[index], - Params: pd, - }) + go func() { + var i int + for ; ; i++ { + select { + case <-ctx.Done(): + return + case <-ch: + pd := portsData[i] + // TODO: handle error + err := testServer.WriteToObjectStream(rpcMessage{ + Method: eventResponses[i], + Params: pd, + }) + if err != nil { + panic(err) } } - }(index, pd) - } + } + }() mockApi := &apiClientMock{ GetCodespaceFunc: func(ctx context.Context, codespaceName string, includeConnection bool) (*api.Codespace, error) { diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 5187dc8de..e50e2576b 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -13,19 +13,17 @@ import ( type rpcClient struct { *jsonrpc2.Conn - conn io.ReadWriteCloser - - eventHandlersMu sync.RWMutex - eventHandlers map[string]chan []byte + conn io.ReadWriteCloser + requestHandler *requestHandler } func newRPCClient(conn io.ReadWriteCloser) *rpcClient { - return &rpcClient{conn: conn, eventHandlers: make(map[string]chan []byte)} + return &rpcClient{conn: conn, requestHandler: newRequestHandler()} } func (r *rpcClient) connect(ctx context.Context) { stream := jsonrpc2.NewBufferedStream(r.conn, jsonrpc2.VSCodeObjectCodec{}) - r.Conn = jsonrpc2.NewConn(ctx, stream, newRequestHandler(r)) + r.Conn = jsonrpc2.NewConn(ctx, stream, r.requestHandler) } func (r *rpcClient) do(ctx context.Context, method string, args, result interface{}) error { @@ -44,7 +42,16 @@ func (r *rpcClient) do(ctx context.Context, method string, args, result interfac return waiter.Wait(waitCtx, result) } -func (r *rpcClient) registerEventHandler(eventName string) chan []byte { +type requestHandler struct { + eventHandlersMu sync.RWMutex + eventHandlers map[string]chan []byte +} + +func newRequestHandler() *requestHandler { + return &requestHandler{eventHandlers: make(map[string]chan []byte)} +} + +func (r *requestHandler) registerEvent(eventName string) chan []byte { r.eventHandlersMu.Lock() defer r.eventHandlersMu.Unlock() @@ -57,23 +64,19 @@ func (r *rpcClient) registerEventHandler(eventName string) chan []byte { return ch } -func (r *rpcClient) eventHandler(eventName string) chan []byte { +func (r *requestHandler) eventHandler(eventName string) chan []byte { r.eventHandlersMu.RLock() defer r.eventHandlersMu.RUnlock() return r.eventHandlers[eventName] } -type requestHandler struct { - rpcClient *rpcClient -} - -func newRequestHandler(rpcClient *rpcClient) *requestHandler { - return &requestHandler{rpcClient: rpcClient} -} - -func (e *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - handler := e.rpcClient.eventHandler(req.Method) +func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + fmt.Println(req.Method) + if req.Params != nil { + fmt.Println(string(*req.Params)) + } + handler := r.eventHandler(req.Method) if handler == nil { return // noop } diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index 715d24fc6..25e2143ab 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -78,8 +78,8 @@ func (s *Session) UpdateSharedServerPrivacy(ctx context.Context, port int, visib return nil } -func (s *Session) WaitForEvent(eventName string) chan []byte { - return s.rpc.registerEventHandler(eventName) +func (s *Session) RegisterEvent(eventName string) chan []byte { + return s.rpc.requestHandler.registerEvent(eventName) } // StartsSSHServer starts an SSH server in the container, installing sshd if necessary, diff --git a/pkg/liveshare/ssh.go b/pkg/liveshare/ssh.go index e7de9055a..ec32671be 100644 --- a/pkg/liveshare/ssh.go +++ b/pkg/liveshare/ssh.go @@ -50,6 +50,7 @@ func (s *sshSession) connect(ctx context.Context) error { return fmt.Errorf("error creating ssh client connection: %w", err) } s.conn = sshClientConn + go s.handleGlobalRequests(reqs) sshClient := ssh.NewClient(sshClientConn, chans, reqs) s.Session, err = sshClient.NewSession() @@ -70,6 +71,15 @@ func (s *sshSession) connect(ctx context.Context) error { return nil } +func (s *sshSession) handleGlobalRequests(incoming <-chan *ssh.Request) { + for r := range incoming { + fmt.Println(r.Type) + // This handles keepalive messages and matches + // the behaviour of OpenSSH. + r.Reply(false, nil) + } +} + func (s *sshSession) Read(p []byte) (n int, err error) { return s.reader.Read(p) } From 9556c72ecfd61dd06aaae471ee65c7e5f39df135 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Thu, 17 Feb 2022 17:05:56 -0500 Subject: [PATCH 07/75] Update event types and drop idea of global request --- pkg/cmd/codespace/ports.go | 4 ++-- pkg/liveshare/rpc.go | 3 --- pkg/liveshare/ssh.go | 10 ---------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 3b698494a..76ad97d74 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -273,8 +273,8 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar } defer safeClose(session, &err) - success := session.RegisterEvent("sharingSucceeded") - failure := session.RegisterEvent("sharingFailed") + success := session.RegisterEvent("serverSharing.sharingSucceeded") + failure := session.RegisterEvent("serverSharing.sharingFailed") // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index e50e2576b..b5d520313 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -73,9 +73,6 @@ func (r *requestHandler) eventHandler(eventName string) chan []byte { func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { fmt.Println(req.Method) - if req.Params != nil { - fmt.Println(string(*req.Params)) - } handler := r.eventHandler(req.Method) if handler == nil { return // noop diff --git a/pkg/liveshare/ssh.go b/pkg/liveshare/ssh.go index ec32671be..e7de9055a 100644 --- a/pkg/liveshare/ssh.go +++ b/pkg/liveshare/ssh.go @@ -50,7 +50,6 @@ func (s *sshSession) connect(ctx context.Context) error { return fmt.Errorf("error creating ssh client connection: %w", err) } s.conn = sshClientConn - go s.handleGlobalRequests(reqs) sshClient := ssh.NewClient(sshClientConn, chans, reqs) s.Session, err = sshClient.NewSession() @@ -71,15 +70,6 @@ func (s *sshSession) connect(ctx context.Context) error { return nil } -func (s *sshSession) handleGlobalRequests(incoming <-chan *ssh.Request) { - for r := range incoming { - fmt.Println(r.Type) - // This handles keepalive messages and matches - // the behaviour of OpenSSH. - r.Reply(false, nil) - } -} - func (s *sshSession) Read(p []byte) (n int, err error) { return s.reader.Read(p) } From 3847d965da2118c9391eb9bf5c3291640986264b Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Fri, 25 Feb 2022 08:09:12 -0500 Subject: [PATCH 08/75] Refactor port update data into liveshare --- pkg/cmd/codespace/ports.go | 61 +++++++++++++------------ pkg/cmd/codespace/ports_test.go | 79 ++++++++++++++++++++------------- pkg/liveshare/rpc.go | 40 ++++++++--------- pkg/liveshare/rpc_test.go | 55 ----------------------- pkg/liveshare/session.go | 21 ++++++++- 5 files changed, 115 insertions(+), 141 deletions(-) delete mode 100644 pkg/liveshare/rpc_test.go diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 76ad97d74..a717c339d 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -18,6 +18,7 @@ import ( "github.com/cli/cli/v2/pkg/liveshare" "github.com/cli/cli/v2/utils" "github.com/muhammadmuzzammil1998/jsonc" + "github.com/sourcegraph/jsonrpc2" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) @@ -273,8 +274,19 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar } defer safeClose(session, &err) - success := session.RegisterEvent("serverSharing.sharingSucceeded") - failure := session.RegisterEvent("serverSharing.sharingFailed") + notificationUpdate := make(chan portUpdateNotification) + h := func(success bool) func(*jsonrpc2.Request) { + return func(req *jsonrpc2.Request) { + var notification portUpdateNotification + if err := json.Unmarshal(*req.Params, ¬ification); err != nil { + return + } + notification.success = success + notificationUpdate <- notification + } + } + session.RegisterRequestHandler("serverSharing.sharingSucceeded", h(true)) + session.RegisterRequestHandler("serverSharing.sharingFailed", h(false)) // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { @@ -289,7 +301,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - if err := a.waitForPortUpdate(ctx, success, failure, session, port.number); err != nil { + if err := a.waitForPortUpdate(ctx, notificationUpdate, session, port.number); err != nil { return newErrUpdatingPortVisibility(port.number, port.visibility, err) } @@ -299,43 +311,30 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return nil } -type portChangeKind string - -const ( - portChangeKindUpdate portChangeKind = "update" -) - -type portData struct { - Port int `json:"port"` - ChangeKind portChangeKind `json:"changeKind"` - ErrorDetail string `json:"errorDetail"` - StatusCode int `json:"statusCode"` +type portUpdateNotification struct { + liveshare.PortUpdate + success bool } var errUpdatePortVisibilityForbidden = errors.New("organization admin has forbidden this privacy setting") -func (a *App) waitForPortUpdate(ctx context.Context, success, failure chan []byte, session *liveshare.Session, port int) error { +func (a *App) waitForPortUpdate(ctx context.Context, n chan portUpdateNotification, session *liveshare.Session, port int) error { for { - var pd portData select { case <-ctx.Done(): return fmt.Errorf("timeout waiting for server sharing to succeed or fail") - case b := <-success: - if err := json.Unmarshal(b, &pd); err != nil { - return fmt.Errorf("error unmarshaling port data: %w", err) - } - if pd.Port == port && pd.ChangeKind == portChangeKindUpdate { - return nil - } - case b := <-failure: - if err := json.Unmarshal(b, &pd); err != nil { - return fmt.Errorf("error unmarshaling port data: %w", err) - } - if pd.Port == port && pd.ChangeKind == portChangeKindUpdate { - if pd.StatusCode == http.StatusForbidden { - return errUpdatePortVisibilityForbidden + case update := <-n: + if update.success { + if update.Port == port && update.ChangeKind == liveshare.PortChangeKindUpdate { + return nil + } + } else { + if update.Port == port && update.ChangeKind == liveshare.PortChangeKindUpdate { + if update.StatusCode == http.StatusForbidden { + return errUpdatePortVisibilityForbidden + } + return errors.New(update.ErrorDetail) } - return errors.New(pd.ErrorDetail) } } } diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index 9f4a3f839..e41e6feed 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/liveshare" livesharetest "github.com/cli/cli/v2/pkg/liveshare/test" "github.com/sourcegraph/jsonrpc2" ) @@ -26,18 +27,24 @@ func TestPortsUpdateVisibilitySuccess(t *testing.T) { } eventResponses := []string{ - "sharingSucceeded", - "sharingSucceeded", + "serverSharing.sharingSucceeded", + "serverSharing.sharingSucceeded", } - portsData := []portData{ + portsData := []portUpdateNotification{ { - Port: 80, - ChangeKind: portChangeKindUpdate, + success: true, + PortUpdate: liveshare.PortUpdate{ + Port: 80, + ChangeKind: liveshare.PortChangeKindUpdate, + }, }, { - Port: 9999, - ChangeKind: portChangeKindUpdate, + success: true, + PortUpdate: liveshare.PortUpdate{ + Port: 9999, + ChangeKind: liveshare.PortChangeKindUpdate, + }, }, } @@ -61,20 +68,26 @@ func TestPortsUpdateVisibilityFailure403(t *testing.T) { } eventResponses := []string{ - "sharingSucceeded", - "sharingFailed", + "serverSharing.sharingSucceeded", + "serverSharing.sharingFailed", } - portsData := []portData{ + portsData := []portUpdateNotification{ { - Port: 80, - ChangeKind: portChangeKindUpdate, + success: true, + PortUpdate: liveshare.PortUpdate{ + Port: 80, + ChangeKind: liveshare.PortChangeKindUpdate, + }, }, { - Port: 9999, - ChangeKind: portChangeKindUpdate, - ErrorDetail: "test error", - StatusCode: 403, + success: false, + PortUpdate: liveshare.PortUpdate{ + Port: 9999, + ChangeKind: liveshare.PortChangeKindUpdate, + ErrorDetail: "test error", + StatusCode: 403, + }, }, } @@ -101,19 +114,25 @@ func TestPortsUpdateVisibilityFailure(t *testing.T) { } eventResponses := []string{ - "sharingSucceeded", - "sharingFailed", + "serverSharing.sharingSucceeded", + "serverSharing.sharingFailed", } - portsData := []portData{ + portsData := []portUpdateNotification{ { - Port: 80, - ChangeKind: portChangeKindUpdate, + success: true, + PortUpdate: liveshare.PortUpdate{ + Port: 80, + ChangeKind: liveshare.PortChangeKindUpdate, + }, }, { - Port: 9999, - ChangeKind: portChangeKindUpdate, - ErrorDetail: "test error", + success: false, + PortUpdate: liveshare.PortUpdate{ + Port: 9999, + ChangeKind: liveshare.PortChangeKindUpdate, + ErrorDetail: "test error", + }, }, } @@ -132,7 +151,7 @@ type joinWorkspaceResult struct { SessionNumber int `json:"sessionNumber"` } -func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses []string, portsData []portData) error { +func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses []string, portsData []portUpdateNotification) error { joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } @@ -163,7 +182,7 @@ func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses [ type rpcMessage struct { Method string - Params portData + Params liveshare.PortUpdate } go func() { @@ -174,14 +193,10 @@ func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses [ return case <-ch: pd := portsData[i] - // TODO: handle error - err := testServer.WriteToObjectStream(rpcMessage{ + _ := testServer.WriteToObjectStream(rpcMessage{ Method: eventResponses[i], - Params: pd, + Params: pd.PortUpdate, }) - if err != nil { - panic(err) - } } } }() diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index b5d520313..293cc4b00 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -42,41 +42,39 @@ func (r *rpcClient) do(ctx context.Context, method string, args, result interfac return waiter.Wait(waitCtx, result) } +type handlerFn func(req *jsonrpc2.Request) + type requestHandler struct { - eventHandlersMu sync.RWMutex - eventHandlers map[string]chan []byte + handlersMu sync.RWMutex + handlers map[string]handlerFn } func newRequestHandler() *requestHandler { - return &requestHandler{eventHandlers: make(map[string]chan []byte)} + return &requestHandler{handlers: make(map[string]handlerFn)} } -func (r *requestHandler) registerEvent(eventName string) chan []byte { - r.eventHandlersMu.Lock() - defer r.eventHandlersMu.Unlock() +func (r *requestHandler) register(requestType string, handler handlerFn) { + r.handlersMu.Lock() + defer r.handlersMu.Unlock() - if ch, ok := r.eventHandlers[eventName]; ok { - return ch + if _, ok := r.handlers[requestType]; ok { + return } - ch := make(chan []byte) - r.eventHandlers[eventName] = ch - return ch + r.handlers[requestType] = handler } -func (r *requestHandler) eventHandler(eventName string) chan []byte { - r.eventHandlersMu.RLock() - defer r.eventHandlersMu.RUnlock() +func (r *requestHandler) handler(requestType string) handlerFn { + r.handlersMu.RLock() + defer r.handlersMu.RUnlock() - return r.eventHandlers[eventName] + return r.handlers[requestType] } func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - fmt.Println(req.Method) - handler := r.eventHandler(req.Method) - if handler == nil { - return // noop + if handler := r.handler(req.Method); handler != nil { + go func() { + handler(req) + }() } - - handler <- *req.Params } diff --git a/pkg/liveshare/rpc_test.go b/pkg/liveshare/rpc_test.go deleted file mode 100644 index 0f78b8780..000000000 --- a/pkg/liveshare/rpc_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package liveshare - -import ( - "context" - "encoding/json" - "fmt" - "net" - "testing" - - "github.com/sourcegraph/jsonrpc2" -) - -func TestRequestHandler(t *testing.T) { - r, w := net.Pipe() - client := newRPCClient(r) - - ctx := context.Background() - client.connect(ctx) - - type params struct { - Data string `json:"data"` - } - - ev := client.registerEventHandler("testEvent") - done := make(chan error) - go func() { - b := <-ev - var receivedParams params - if err := json.Unmarshal(b, &receivedParams); err != nil { - done <- err - return - } - if receivedParams.Data != "test" { - done <- fmt.Errorf("expected test, got %q", receivedParams.Data) - } - done <- nil - }() - - go func() { - codec := jsonrpc2.VSCodeObjectCodec{} - type message struct { - Method string `json:"method"` - Params params `json:"params"` - } - codec.WriteObject(w, message{ - Method: "testEvent", - Params: params{"test"}, - }) - }() - - err := <-done - if err != nil { - t.Fatal(err) - } -} diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index 25e2143ab..9095b2f0d 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -44,6 +44,19 @@ type Port struct { Privacy string `json:"privacy"` } +type PortChangeKind string + +const ( + PortChangeKindUpdate PortChangeKind = "update" +) + +type PortUpdate struct { + Port int `json:"port"` + ChangeKind PortChangeKind `json:"changeKind"` + ErrorDetail string `json:"errorDetail"` + StatusCode int `json:"statusCode"` +} + // startSharing tells the Live Share host to start sharing the specified port from the container. // The sessionName describes the purpose of the remote port or service. // It returns an identifier that can be used to open an SSH channel to the remote port. @@ -78,8 +91,12 @@ func (s *Session) UpdateSharedServerPrivacy(ctx context.Context, port int, visib return nil } -func (s *Session) RegisterEvent(eventName string) chan []byte { - return s.rpc.requestHandler.registerEvent(eventName) +// RegisterRequestHandler allows the caller to register a jsonrpc request handler +// for a given request type. The handler will be called when the request is received +// by the session's RPC server. If the request type has already been registered, the function will +// noop. +func (s *Session) RegisterRequestHandler(requestType string, h handlerFn) { + s.rpc.requestHandler.register(requestType, h) } // StartsSSHServer starts an SSH server in the container, installing sshd if necessary, From 2328ccb8812c9535050d125cd3e2a8baa37c9795 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 28 Feb 2022 07:56:37 -0500 Subject: [PATCH 09/75] Abstract port notification logic into liveshare --- pkg/cmd/codespace/ports.go | 83 ++++++++------------ pkg/cmd/codespace/ports_test.go | 22 +++--- pkg/liveshare/ports.go | 135 ++++++++++++++++++++++++++++++++ pkg/liveshare/rpc.go | 18 ++--- pkg/liveshare/session.go | 69 +--------------- 5 files changed, 190 insertions(+), 137 deletions(-) create mode 100644 pkg/liveshare/ports.go diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index a717c339d..181f44b79 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -18,7 +18,6 @@ import ( "github.com/cli/cli/v2/pkg/liveshare" "github.com/cli/cli/v2/utils" "github.com/muhammadmuzzammil1998/jsonc" - "github.com/sourcegraph/jsonrpc2" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) @@ -254,6 +253,8 @@ func (e *ErrUpdatingPortVisibility) Unwrap() error { return e.err } +var errUpdatePortVisibilityForbidden = errors.New("organization admin has forbidden this privacy setting") + func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, args []string) (err error) { ports, err := a.parsePortVisibilities(args) if err != nil { @@ -274,35 +275,46 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar } defer safeClose(session, &err) - notificationUpdate := make(chan portUpdateNotification) - h := func(success bool) func(*jsonrpc2.Request) { - return func(req *jsonrpc2.Request) { - var notification portUpdateNotification - if err := json.Unmarshal(*req.Params, ¬ification); err != nil { - return - } - notification.success = success - notificationUpdate <- notification - } - } - session.RegisterRequestHandler("serverSharing.sharingSucceeded", h(true)) - session.RegisterRequestHandler("serverSharing.sharingFailed", h(false)) + errc := make(chan error, 1) // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility)) - err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) - - if err != nil { - return fmt.Errorf("error updating port %d to %s: %w", port.number, port.visibility, err) - } // wait for success or failure ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - if err := a.waitForPortUpdate(ctx, notificationUpdate, session, port.number); err != nil { - return newErrUpdatingPortVisibility(port.number, port.visibility, err) + go func() { + updateNotif, err := session.WaitForPortNotification(ctx, port.number, liveshare.PortChangeKindUpdate) + if err != nil { + errc <- fmt.Errorf("error waiting for port %d to update: %w", port.number, err) + return + } + if !updateNotif.Success { + if updateNotif.StatusCode == http.StatusForbidden { + errc <- newErrUpdatingPortVisibility(port.number, port.visibility, errUpdatePortVisibilityForbidden) + return + } + errc <- newErrUpdatingPortVisibility(port.number, port.visibility, errors.New(updateNotif.ErrorDetail)) + return + } + errc <- nil // success + }() + + err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) + if err != nil { + return fmt.Errorf("error updating port %d to %s: %w", port.number, port.visibility, err) + } + + // wait for success or failure + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errc: + if err != nil { + return err + } } a.StopProgressIndicator() @@ -311,35 +323,6 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return nil } -type portUpdateNotification struct { - liveshare.PortUpdate - success bool -} - -var errUpdatePortVisibilityForbidden = errors.New("organization admin has forbidden this privacy setting") - -func (a *App) waitForPortUpdate(ctx context.Context, n chan portUpdateNotification, session *liveshare.Session, port int) error { - for { - select { - case <-ctx.Done(): - return fmt.Errorf("timeout waiting for server sharing to succeed or fail") - case update := <-n: - if update.success { - if update.Port == port && update.ChangeKind == liveshare.PortChangeKindUpdate { - return nil - } - } else { - if update.Port == port && update.ChangeKind == liveshare.PortChangeKindUpdate { - if update.StatusCode == http.StatusForbidden { - return errUpdatePortVisibilityForbidden - } - return errors.New(update.ErrorDetail) - } - } - } - } -} - type portVisibility struct { number int visibility string diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index e41e6feed..b7563bb80 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -31,16 +31,16 @@ func TestPortsUpdateVisibilitySuccess(t *testing.T) { "serverSharing.sharingSucceeded", } - portsData := []portUpdateNotification{ + portsData := []liveshare.PortNotification{ { - success: true, + Success: true, PortUpdate: liveshare.PortUpdate{ Port: 80, ChangeKind: liveshare.PortChangeKindUpdate, }, }, { - success: true, + Success: true, PortUpdate: liveshare.PortUpdate{ Port: 9999, ChangeKind: liveshare.PortChangeKindUpdate, @@ -72,16 +72,16 @@ func TestPortsUpdateVisibilityFailure403(t *testing.T) { "serverSharing.sharingFailed", } - portsData := []portUpdateNotification{ + portsData := []liveshare.PortNotification{ { - success: true, + Success: true, PortUpdate: liveshare.PortUpdate{ Port: 80, ChangeKind: liveshare.PortChangeKindUpdate, }, }, { - success: false, + Success: false, PortUpdate: liveshare.PortUpdate{ Port: 9999, ChangeKind: liveshare.PortChangeKindUpdate, @@ -118,16 +118,16 @@ func TestPortsUpdateVisibilityFailure(t *testing.T) { "serverSharing.sharingFailed", } - portsData := []portUpdateNotification{ + portsData := []liveshare.PortNotification{ { - success: true, + Success: true, PortUpdate: liveshare.PortUpdate{ Port: 80, ChangeKind: liveshare.PortChangeKindUpdate, }, }, { - success: false, + Success: false, PortUpdate: liveshare.PortUpdate{ Port: 9999, ChangeKind: liveshare.PortChangeKindUpdate, @@ -151,7 +151,7 @@ type joinWorkspaceResult struct { SessionNumber int `json:"sessionNumber"` } -func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses []string, portsData []portUpdateNotification) error { +func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses []string, portsData []liveshare.PortNotification) error { joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } @@ -193,7 +193,7 @@ func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses [ return case <-ch: pd := portsData[i] - _ := testServer.WriteToObjectStream(rpcMessage{ + testServer.WriteToObjectStream(rpcMessage{ Method: eventResponses[i], Params: pd.PortUpdate, }) diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go new file mode 100644 index 000000000..340a74554 --- /dev/null +++ b/pkg/liveshare/ports.go @@ -0,0 +1,135 @@ +package liveshare + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/sourcegraph/jsonrpc2" +) + +// Port describes a port exposed by the container. +type Port struct { + SourcePort int `json:"sourcePort"` + DestinationPort int `json:"destinationPort"` + SessionName string `json:"sessionName"` + StreamName string `json:"streamName"` + StreamCondition string `json:"streamCondition"` + BrowseURL string `json:"browseUrl"` + IsPublic bool `json:"isPublic"` + IsTCPServerConnectionEstablished bool `json:"isTCPServerConnectionEstablished"` + HasTLSHandshakePassed bool `json:"hasTLSHandshakePassed"` + Privacy string `json:"privacy"` +} + +type PortChangeKind string + +const ( + PortChangeKindStart PortChangeKind = "start" + PortChangeKindUpdate PortChangeKind = "update" +) + +type PortUpdate struct { + Port int `json:"port"` + ChangeKind PortChangeKind `json:"changeKind"` + ErrorDetail string `json:"errorDetail"` + StatusCode int `json:"statusCode"` +} + +// startSharing tells the Live Share host to start sharing the specified port from the container. +// The sessionName describes the purpose of the remote port or service. +// It returns an identifier that can be used to open an SSH channel to the remote port. +func (s *Session) startSharing(ctx context.Context, sessionName string, port int) (channelID, error) { + args := []interface{}{port, sessionName, fmt.Sprintf("http://localhost:%d", port)} + errc := make(chan error, 1) + + go func() { + startNotification, err := s.WaitForPortNotification(ctx, port, PortChangeKindStart) + if err != nil { + errc <- fmt.Errorf("error while waiting for port notification: %w", err) + return + } + if !startNotification.Success { + errc <- fmt.Errorf("error while starting port sharing: %s", startNotification.ErrorDetail) + return + } + errc <- nil // success + }() + + var response Port + if err := s.rpc.do(ctx, "serverSharing.startSharing", args, &response); err != nil { + return channelID{}, err + } + + select { + case <-ctx.Done(): + return channelID{}, ctx.Err() + case err := <-errc: + if err != nil { + return channelID{}, err + } + } + + return channelID{response.StreamName, response.StreamCondition}, nil +} + +type PortNotification struct { + PortUpdate + Success bool +} + +// WaitForPortNotification waits for a port notification to be received. It returns the notification +// or an error if the notification is not received before the context is cancelled or it fails +// to parse the notification. +func (s *Session) WaitForPortNotification(ctx context.Context, port int, notifType PortChangeKind) (*PortNotification, error) { + notificationUpdate := make(chan PortNotification, 1) + errc := make(chan error, 1) + + h := func(success bool) func(*jsonrpc2.Request) { + return func(req *jsonrpc2.Request) { + var notification PortNotification + if err := json.Unmarshal(*req.Params, ¬ification); err != nil { + errc <- fmt.Errorf("error unmarshaling notification: %w", err) + return + } + notification.Success = success + notificationUpdate <- notification + } + } + s.registerRequestHandler("serverSharing.sharingSucceeded", h(true)) + s.registerRequestHandler("serverSharing.sharingFailed", h(false)) + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-errc: + return nil, err + case notification := <-notificationUpdate: + if notification.Port == port && notification.ChangeKind == notifType { + return ¬ification, nil + } + } + } +} + +// GetSharedServers returns a description of each container port +// shared by a prior call to StartSharing by some client. +func (s *Session) GetSharedServers(ctx context.Context) ([]*Port, error) { + var response []*Port + if err := s.rpc.do(ctx, "serverSharing.getSharedServers", []string{}, &response); err != nil { + return nil, err + } + + return response, nil +} + +// UpdateSharedServerPrivacy controls port permissions and visibility scopes for who can access its URLs +// in the browser. +func (s *Session) UpdateSharedServerPrivacy(ctx context.Context, port int, visibility string) error { + if err := s.rpc.do(ctx, "serverSharing.updateSharedServerPrivacy", []interface{}{port, visibility}, nil); err != nil { + return err + } + + return nil +} diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 293cc4b00..ee0c196c4 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -46,25 +46,25 @@ type handlerFn func(req *jsonrpc2.Request) type requestHandler struct { handlersMu sync.RWMutex - handlers map[string]handlerFn + handlers map[string][]handlerFn } func newRequestHandler() *requestHandler { - return &requestHandler{handlers: make(map[string]handlerFn)} + return &requestHandler{handlers: make(map[string][]handlerFn)} } func (r *requestHandler) register(requestType string, handler handlerFn) { r.handlersMu.Lock() defer r.handlersMu.Unlock() - if _, ok := r.handlers[requestType]; ok { - return + if _, ok := r.handlers[requestType]; !ok { + r.handlers[requestType] = []handlerFn{} } - r.handlers[requestType] = handler + r.handlers[requestType] = append(r.handlers[requestType], handler) } -func (r *requestHandler) handler(requestType string) handlerFn { +func (r *requestHandler) handlerFn(requestType string) []handlerFn { r.handlersMu.RLock() defer r.handlersMu.RUnlock() @@ -72,9 +72,7 @@ func (r *requestHandler) handler(requestType string) handlerFn { } func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - if handler := r.handler(req.Method); handler != nil { - go func() { - handler(req) - }() + for _, handler := range r.handlerFn(req.Method) { + go handler(req) } } diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index 9095b2f0d..6249acb23 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -30,72 +30,9 @@ func (s *Session) Close() error { return nil } -// Port describes a port exposed by the container. -type Port struct { - SourcePort int `json:"sourcePort"` - DestinationPort int `json:"destinationPort"` - SessionName string `json:"sessionName"` - StreamName string `json:"streamName"` - StreamCondition string `json:"streamCondition"` - BrowseURL string `json:"browseUrl"` - IsPublic bool `json:"isPublic"` - IsTCPServerConnectionEstablished bool `json:"isTCPServerConnectionEstablished"` - HasTLSHandshakePassed bool `json:"hasTLSHandshakePassed"` - Privacy string `json:"privacy"` -} - -type PortChangeKind string - -const ( - PortChangeKindUpdate PortChangeKind = "update" -) - -type PortUpdate struct { - Port int `json:"port"` - ChangeKind PortChangeKind `json:"changeKind"` - ErrorDetail string `json:"errorDetail"` - StatusCode int `json:"statusCode"` -} - -// startSharing tells the Live Share host to start sharing the specified port from the container. -// The sessionName describes the purpose of the remote port or service. -// It returns an identifier that can be used to open an SSH channel to the remote port. -func (s *Session) startSharing(ctx context.Context, sessionName string, port int) (channelID, error) { - args := []interface{}{port, sessionName, fmt.Sprintf("http://localhost:%d", port)} - var response Port - if err := s.rpc.do(ctx, "serverSharing.startSharing", args, &response); err != nil { - return channelID{}, err - } - - return channelID{response.StreamName, response.StreamCondition}, nil -} - -// GetSharedServers returns a description of each container port -// shared by a prior call to StartSharing by some client. -func (s *Session) GetSharedServers(ctx context.Context) ([]*Port, error) { - var response []*Port - if err := s.rpc.do(ctx, "serverSharing.getSharedServers", []string{}, &response); err != nil { - return nil, err - } - - return response, nil -} - -// UpdateSharedServerPrivacy controls port permissions and visibility scopes for who can access its URLs -// in the browser. -func (s *Session) UpdateSharedServerPrivacy(ctx context.Context, port int, visibility string) error { - if err := s.rpc.do(ctx, "serverSharing.updateSharedServerPrivacy", []interface{}{port, visibility}, nil); err != nil { - return err - } - - return nil -} - -// RegisterRequestHandler allows the caller to register a jsonrpc request handler -// for a given request type. The handler will be called when the request is received -// by the session's RPC server. If the request type has already been registered, the function will -// noop. -func (s *Session) RegisterRequestHandler(requestType string, h handlerFn) { +// registerRequestHandler registers a handler for the given request type with the RPC +// server. +func (s *Session) registerRequestHandler(requestType string, h handlerFn) { s.rpc.requestHandler.register(requestType, h) } From 35638cb82f76871224343584c6911df61e0c4e6b Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 28 Feb 2022 08:22:09 -0500 Subject: [PATCH 10/75] Update tests for serverSharing --- pkg/liveshare/port_forwarder_test.go | 16 ++++++++++++++-- pkg/liveshare/ports.go | 4 ++-- pkg/liveshare/rpc.go | 4 ++-- pkg/liveshare/session_test.go | 23 +++++++++++++++++++++-- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index 0923847e0..f68c11aba 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -28,7 +28,13 @@ func TestNewPortForwarder(t *testing.T) { func TestPortForwarderStart(t *testing.T) { streamName, streamCondition := "stream-name", "stream-condition" + port := 8000 + sendNotification := make(chan PortUpdate) serverSharing := func(req *jsonrpc2.Request) (interface{}, error) { + sendNotification <- PortUpdate{ + Port: int(port), + ChangeKind: PortChangeKindStart, + } return Port{StreamName: streamName, StreamCondition: streamCondition}, nil } getStream := func(req *jsonrpc2.Request) (interface{}, error) { @@ -55,10 +61,16 @@ func TestPortForwarderStart(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + go func() { + testServer.WriteToObjectStream(rpcPortTestMessage{ + Method: "serverSharing.sharingSucceeded", + Params: <-sendNotification, + }) + }() + done := make(chan error) go func() { - const name, remote = "ssh", 8000 - done <- NewPortForwarder(session, name, remote, false).ForwardToListener(ctx, listen) + done <- NewPortForwarder(session, "ssh", port, false).ForwardToListener(ctx, listen) }() go func() { diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index 340a74554..9f9bd36f1 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -85,8 +85,8 @@ func (s *Session) WaitForPortNotification(ctx context.Context, port int, notifTy notificationUpdate := make(chan PortNotification, 1) errc := make(chan error, 1) - h := func(success bool) func(*jsonrpc2.Request) { - return func(req *jsonrpc2.Request) { + h := func(success bool) func(*jsonrpc2.Conn, *jsonrpc2.Request) { + return func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) { var notification PortNotification if err := json.Unmarshal(*req.Params, ¬ification); err != nil { errc <- fmt.Errorf("error unmarshaling notification: %w", err) diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index ee0c196c4..a32d8507a 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -42,7 +42,7 @@ func (r *rpcClient) do(ctx context.Context, method string, args, result interfac return waiter.Wait(waitCtx, result) } -type handlerFn func(req *jsonrpc2.Request) +type handlerFn func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) type requestHandler struct { handlersMu sync.RWMutex @@ -73,6 +73,6 @@ func (r *requestHandler) handlerFn(requestType string) []handlerFn { func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { for _, handler := range r.handlerFn(req.Method) { - go handler(req) + go handler(conn, req) } } diff --git a/pkg/liveshare/session_test.go b/pkg/liveshare/session_test.go index 44cb2357b..cfdfa5815 100644 --- a/pkg/liveshare/session_test.go +++ b/pkg/liveshare/session_test.go @@ -49,8 +49,14 @@ func makeMockSession(opts ...livesharetest.ServerOption) (*livesharetest.Server, return testServer, session, nil } +type rpcPortTestMessage struct { + Method string + Params PortUpdate +} + func TestServerStartSharing(t *testing.T) { serverPort, serverProtocol := 2222, "sshd" + sendNotification := make(chan PortUpdate) startSharing := func(req *jsonrpc2.Request) (interface{}, error) { var args []interface{} if err := json.Unmarshal(*req.Params, &args); err != nil { @@ -59,9 +65,11 @@ func TestServerStartSharing(t *testing.T) { if len(args) < 3 { return nil, errors.New("not enough arguments to start sharing") } - if port, ok := args[0].(float64); !ok { + port, ok := args[0].(float64) + if !ok { return nil, errors.New("port argument is not an int") - } else if port != float64(serverPort) { + } + if port != float64(serverPort) { return nil, errors.New("port does not match serverPort") } if protocol, ok := args[1].(string); !ok { @@ -74,6 +82,10 @@ func TestServerStartSharing(t *testing.T) { } else if browseURL != fmt.Sprintf("http://localhost:%d", serverPort) { return nil, errors.New("browseURL does not match expected") } + sendNotification <- PortUpdate{ + Port: int(port), + ChangeKind: PortChangeKindStart, + } return Port{StreamName: "stream-name", StreamCondition: "stream-condition"}, nil } testServer, session, err := makeMockSession( @@ -86,6 +98,13 @@ func TestServerStartSharing(t *testing.T) { } ctx := context.Background() + go func() { + testServer.WriteToObjectStream(rpcPortTestMessage{ + Method: "serverSharing.sharingSucceeded", + Params: <-sendNotification, + }) + }() + done := make(chan error) go func() { streamID, err := session.startSharing(ctx, serverProtocol, serverPort) From 347e7dc67bb249bc1ab6cc2c49cc79423f02bf49 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Wed, 2 Mar 2022 01:22:05 +0000 Subject: [PATCH 11/75] delete unneeded comment --- pkg/liveshare/port_forwarder.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/liveshare/port_forwarder.go b/pkg/liveshare/port_forwarder.go index 546201f12..ba2c7ff40 100644 --- a/pkg/liveshare/port_forwarder.go +++ b/pkg/liveshare/port_forwarder.go @@ -98,8 +98,6 @@ func (fwd *PortForwarder) shareRemotePort(ctx context.Context) (channelID, error err = fmt.Errorf("failed to share remote port %d: %w", fwd.remotePort, err) } - // wait for port change kind start - return id, err } From 383a7119029d1bacc34a6b4afe293926e7726327 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Wed, 2 Mar 2022 17:59:46 +0000 Subject: [PATCH 12/75] fix linter error --- pkg/cmd/codespace/ports_test.go | 2 +- pkg/liveshare/port_forwarder_test.go | 2 +- pkg/liveshare/session_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index b7563bb80..dc858454e 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -193,7 +193,7 @@ func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses [ return case <-ch: pd := portsData[i] - testServer.WriteToObjectStream(rpcMessage{ + _ = testServer.WriteToObjectStream(rpcMessage{ Method: eventResponses[i], Params: pd.PortUpdate, }) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index f68c11aba..464b17a34 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -62,7 +62,7 @@ func TestPortForwarderStart(t *testing.T) { defer cancel() go func() { - testServer.WriteToObjectStream(rpcPortTestMessage{ + _ = testServer.WriteToObjectStream(rpcPortTestMessage{ Method: "serverSharing.sharingSucceeded", Params: <-sendNotification, }) diff --git a/pkg/liveshare/session_test.go b/pkg/liveshare/session_test.go index cfdfa5815..0067e3350 100644 --- a/pkg/liveshare/session_test.go +++ b/pkg/liveshare/session_test.go @@ -99,7 +99,7 @@ func TestServerStartSharing(t *testing.T) { ctx := context.Background() go func() { - testServer.WriteToObjectStream(rpcPortTestMessage{ + _ = testServer.WriteToObjectStream(rpcPortTestMessage{ Method: "serverSharing.sharingSucceeded", Params: <-sendNotification, }) From 7656943c3153a37ffa358a1911867ce3c04057ed Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Thu, 3 Mar 2022 18:40:00 +0000 Subject: [PATCH 13/75] review suggestions --- pkg/cmd/codespace/ports.go | 4 ++-- pkg/cmd/codespace/ports_test.go | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 181f44b79..8d90643fd 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -285,7 +285,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - go func() { + go func(port portVisibility) { updateNotif, err := session.WaitForPortNotification(ctx, port.number, liveshare.PortChangeKindUpdate) if err != nil { errc <- fmt.Errorf("error waiting for port %d to update: %w", port.number, err) @@ -300,7 +300,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return } errc <- nil // success - }() + }(port) err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) if err != nil { diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index dc858454e..1cbf50103 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -48,7 +48,7 @@ func TestPortsUpdateVisibilitySuccess(t *testing.T) { }, } - err := runUpdateVisibilityTest(portVisibilities, eventResponses, portsData) + err := runUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) if err != nil { t.Errorf("unexpected error: %v", err) @@ -91,7 +91,7 @@ func TestPortsUpdateVisibilityFailure403(t *testing.T) { }, } - err := runUpdateVisibilityTest(portVisibilities, eventResponses, portsData) + err := runUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) if err == nil { t.Errorf("unexpected error: %v", err) } @@ -136,7 +136,7 @@ func TestPortsUpdateVisibilityFailure(t *testing.T) { }, } - err := runUpdateVisibilityTest(portVisibilities, eventResponses, portsData) + err := runUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) if err == nil { t.Errorf("unexpected error: %v", err) } @@ -151,7 +151,9 @@ type joinWorkspaceResult struct { SessionNumber int `json:"sessionNumber"` } -func runUpdateVisibilityTest(portVisibilities []portVisibility, eventResponses []string, portsData []liveshare.PortNotification) error { +func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, eventResponses []string, portsData []liveshare.PortNotification) error { + t.Helper() + joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } From c38ca830be5b34dad8a457076ac61fa203e33d0d Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 3 Mar 2022 11:10:22 -0800 Subject: [PATCH 14/75] Extract shared.GetJob so we can use in rerun too --- pkg/cmd/run/shared/shared.go | 12 ++++++++++++ pkg/cmd/run/view/view.go | 14 +------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index b015ecc0e..81cbcc8f4 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -307,6 +307,18 @@ func GetJobs(client *api.Client, repo ghrepo.Interface, run Run) ([]Job, error) return result.Jobs, nil } +func GetJob(client *api.Client, repo ghrepo.Interface, jobID string) (*Job, error) { + path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID) + + var result Job + err := client.REST(repo.RepoHost(), "GET", path, nil, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { var selected int now := time.Now() diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index f41f37801..63518a632 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -183,7 +183,7 @@ func runView(opts *ViewOptions) error { if jobID != "" { opts.IO.StartProgressIndicator() - selectedJob, err = getJob(client, repo, jobID) + selectedJob, err = shared.GetJob(client, repo, jobID) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get job: %w", err) @@ -395,18 +395,6 @@ func runView(opts *ViewOptions) error { return nil } -func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Job, error) { - path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID) - - var result shared.Job - err := client.REST(repo.RepoHost(), "GET", path, nil, &result) - if err != nil { - return nil, err - } - - return &result, nil -} - func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) { req, err := http.NewRequest("GET", logURL, nil) if err != nil { From bf5801e646221e93784ae1db76a75989088b154a Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 3 Mar 2022 11:49:08 -0800 Subject: [PATCH 15/75] Implement `--job` for rerunning a specific actions job --- pkg/cmd/run/rerun/rerun.go | 112 ++++++++++++++++++++++++-------- pkg/cmd/run/rerun/rerun_test.go | 45 ++++++++++++- 2 files changed, 129 insertions(+), 28 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 98cfa7060..21a3c31d1 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -20,6 +20,7 @@ type RerunOptions struct { RunID string OnlyFailed bool + JobID string Prompt bool } @@ -38,12 +39,22 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm // support `-R, --repo` override opts.BaseRepo = f.BaseRepo - if len(args) > 0 { + if len(args) == 0 && opts.JobID == "" { + if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("run or job ID required when not running interactively") + } else { + opts.Prompt = true + } + } else if len(args) > 0 { opts.RunID = args[0] - } else if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("run ID required when not running interactively") - } else { - opts.Prompt = true + } + + if opts.RunID != "" && opts.JobID != "" { + opts.RunID = "" + if opts.IO.CanPrompt() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon()) + } } if runF != nil { @@ -54,6 +65,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies") return cmd } @@ -70,10 +82,23 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to determine base repo: %w", err) } + cs := opts.IO.ColorScheme() + runID := opts.RunID + jobID := opts.JobID + var selectedJob *shared.Job + + if jobID != "" { + opts.IO.StartProgressIndicator() + selectedJob, err = shared.GetJob(client, repo, jobID) + opts.IO.StopProgressIndicator() + if err != nil { + return fmt.Errorf("failed to get job: %w", err) + } + runID = fmt.Sprintf("%d", selectedJob.RunID) + } if opts.Prompt { - cs := opts.IO.ColorScheme() runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool { if run.Status != shared.Completed { return false @@ -94,40 +119,73 @@ func runRerun(opts *RerunOptions) error { } } - opts.IO.StartProgressIndicator() - run, err := shared.GetRun(client, repo, runID) - opts.IO.StopProgressIndicator() - if err != nil { - return fmt.Errorf("failed to get run: %w", err) + if opts.JobID != "" { + err = rerunJob(client, repo, selectedJob) + if err != nil { + return err + } + if opts.IO.CanPrompt() { + fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s\n", + cs.SuccessIcon(), + cs.Cyanf("%d", selectedJob.ID), + cs.Cyanf("%d", selectedJob.RunID)) + } + } else { + opts.IO.StartProgressIndicator() + run, err := shared.GetRun(client, repo, runID) + opts.IO.StopProgressIndicator() + if err != nil { + return fmt.Errorf("failed to get run: %w", err) + } + + err = rerunRun(client, repo, run, opts.OnlyFailed) + if err != nil { + return err + } + if opts.IO.CanPrompt() { + onlyFailedMsg := "" + if opts.OnlyFailed { + onlyFailedMsg = "(failed jobs) " + } + fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n", + cs.SuccessIcon(), + onlyFailedMsg, + cs.Cyanf("%d", run.ID)) + } } + return nil +} + +func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFailed bool) error { runVerb := "rerun" - if opts.OnlyFailed { + if onlyFailed { runVerb = "rerun-failed-jobs" } path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb) - err = client.REST(repo.RepoHost(), "POST", path, nil, nil) + err := client.REST(repo.RepoHost(), "POST", path, nil, nil) if err != nil { var httpError api.HTTPError if errors.As(err, &httpError) && httpError.StatusCode == 403 { - return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken.", run.ID) + return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID) + } + return fmt.Errorf("failed to rerun: %w", err) + } + return nil +} + +func rerunJob(client *api.Client, repo ghrepo.Interface, job *shared.Job) error { + path := fmt.Sprintf("repos/%s/actions/jobs/%d/rerun", ghrepo.FullName(repo), job.ID) + + err := client.REST(repo.RepoHost(), "POST", path, nil, nil) + if err != nil { + var httpError api.HTTPError + if errors.As(err, &httpError) && httpError.StatusCode == 403 { + return fmt.Errorf("job %d cannot be rerun", job.ID) } return fmt.Errorf("failed to rerun: %w", err) } - - if opts.IO.CanPrompt() { - cs := opts.IO.ColorScheme() - onlyFailedMsg := "" - if opts.OnlyFailed { - onlyFailedMsg = "(failed jobs) " - } - fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n", - cs.SuccessIcon(), - onlyFailedMsg, - cs.Cyanf("%d", run.ID)) - } - return nil } diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 3fcf68ff3..094425290 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -67,6 +67,33 @@ func TestNewCmdRerun(t *testing.T) { OnlyFailed: true, }, }, + { + name: "with arg job", + tty: true, + cli: "--job 1234", + wants: RerunOptions{ + JobID: "1234", + }, + }, + { + name: "with args job and runID ignores runID", + tty: true, + cli: "1234 --job 5678", + wants: RerunOptions{ + JobID: "5678", + }, + }, + { + name: "with arg job with no ID fails", + tty: true, + cli: "--job", + wantsErr: true, + }, + { + name: "with arg job with no ID no tty fails", + cli: "--job", + wantsErr: true, + }, } for _, tt := range tests { @@ -151,6 +178,22 @@ func TestRerun(t *testing.T) { }, wantOut: "✓ Requested rerun (failed jobs) of run 1234\n", }, + { + name: "arg including a specific job", + tty: true, + opts: &RerunOptions{ + JobID: "20", // 20 is shared.FailedJob.ID + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), + httpmock.JSONResponse(shared.FailedJob)) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"), + httpmock.StringResponse("{}")) + }, + wantOut: "✓ Requested rerun of job 20 on run 1234\n", + }, { name: "prompt", tty: true, @@ -209,7 +252,7 @@ func TestRerun(t *testing.T) { httpmock.StatusStringResponse(403, "no")) }, wantErr: true, - errOut: "run 3 cannot be rerun; its workflow file may be broken.", + errOut: "run 3 cannot be rerun; its workflow file may be broken", }, } From 625f3ac14482e510865bd32a0eecc23fe8dafb86 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Thu, 3 Mar 2022 15:36:53 -0500 Subject: [PATCH 16/75] Updates wording for codespaces accept permissions flow --- pkg/cmd/codespace/create.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index b145a6a7d..406ad10f6 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -164,7 +164,7 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api } choices := []string{ - "Continue in browser to review and authorize additional permissions", + "Continue in browser to review and authorize additional permissions (Recommended)", "Continue without authorizing additional permissions", } @@ -190,6 +190,7 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api // if the user chose to continue in the browser, open the URL if answers.Accept == choices[0] { + fmt.Fprintln(a.io.ErrOut, "Please re-run the create request after accepting permissions in the browser.") if err := a.browser.Browse(allowPermissionsURL); err != nil { return nil, fmt.Errorf("error opening browser: %w", err) } From 96167069845f5c6fd25d272483f01a38399ffeee Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Fri, 4 Mar 2022 22:30:50 +0000 Subject: [PATCH 17/75] review suggestion --- pkg/cmd/codespace/ports.go | 41 ++++++++++++++++++-------------------- pkg/liveshare/ports.go | 31 ++++++++++++---------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 8d90643fd..06d27e564 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -275,7 +275,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar } defer safeClose(session, &err) - errc := make(chan error, 1) + g, ctx := errgroup.WithContext(ctx) // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { @@ -285,36 +285,33 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - go func(port portVisibility) { + g.Go(func() error { updateNotif, err := session.WaitForPortNotification(ctx, port.number, liveshare.PortChangeKindUpdate) if err != nil { - errc <- fmt.Errorf("error waiting for port %d to update: %w", port.number, err) - return + return fmt.Errorf("error waiting for port %d to update: %w", port.number, err) + } if !updateNotif.Success { if updateNotif.StatusCode == http.StatusForbidden { - errc <- newErrUpdatingPortVisibility(port.number, port.visibility, errUpdatePortVisibilityForbidden) - return + return newErrUpdatingPortVisibility(port.number, port.visibility, errUpdatePortVisibilityForbidden) } - errc <- newErrUpdatingPortVisibility(port.number, port.visibility, errors.New(updateNotif.ErrorDetail)) - return - } - errc <- nil // success - }(port) + return newErrUpdatingPortVisibility(port.number, port.visibility, errors.New(updateNotif.ErrorDetail)) - err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) - if err != nil { - return fmt.Errorf("error updating port %d to %s: %w", port.number, port.visibility, err) - } + } + return nil // success + }) + + g.Go(func() error { + err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility) + if err != nil { + return fmt.Errorf("error updating port %d to %s: %w", port.number, port.visibility, err) + } + return nil + }) // wait for success or failure - select { - case <-ctx.Done(): - return ctx.Err() - case err := <-errc: - if err != nil { - return err - } + if err := g.Wait(); err != nil { + return err } a.StopProgressIndicator() diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index 9f9bd36f1..b20e326a4 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/sourcegraph/jsonrpc2" + "golang.org/x/sync/errgroup" ) // Port describes a port exposed by the container. @@ -41,33 +42,27 @@ type PortUpdate struct { // It returns an identifier that can be used to open an SSH channel to the remote port. func (s *Session) startSharing(ctx context.Context, sessionName string, port int) (channelID, error) { args := []interface{}{port, sessionName, fmt.Sprintf("http://localhost:%d", port)} - errc := make(chan error, 1) + g, ctx := errgroup.WithContext(ctx) - go func() { + g.Go(func() error { startNotification, err := s.WaitForPortNotification(ctx, port, PortChangeKindStart) if err != nil { - errc <- fmt.Errorf("error while waiting for port notification: %w", err) - return + return fmt.Errorf("error while waiting for port notification: %w", err) + } if !startNotification.Success { - errc <- fmt.Errorf("error while starting port sharing: %s", startNotification.ErrorDetail) - return + return fmt.Errorf("error while starting port sharing: %s", startNotification.ErrorDetail) } - errc <- nil // success - }() + return nil // success + }) var response Port - if err := s.rpc.do(ctx, "serverSharing.startSharing", args, &response); err != nil { - return channelID{}, err - } + g.Go(func() error { + return s.rpc.do(ctx, "serverSharing.startSharing", args, &response) + }) - select { - case <-ctx.Done(): - return channelID{}, ctx.Err() - case err := <-errc: - if err != nil { - return channelID{}, err - } + if err := g.Wait(); err != nil { + return channelID{}, err } return channelID{response.StreamName, response.StreamCondition}, nil From 75c6c2c8778d8b13b298746a4fe40ca5a42250c5 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Fri, 4 Mar 2022 23:07:24 +0000 Subject: [PATCH 18/75] fix scoping bug --- pkg/cmd/codespace/ports.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 06d27e564..4109488f9 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -275,13 +275,12 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar } defer safeClose(session, &err) - g, ctx := errgroup.WithContext(ctx) - // TODO: check if port visibility can be updated in parallel instead of sequentially for _, port := range ports { a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility)) // wait for success or failure + g, ctx := errgroup.WithContext(ctx) ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() From fc55e01d8639134b669abd41b115f3f4629c8db0 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Sat, 5 Mar 2022 01:48:11 +0000 Subject: [PATCH 19/75] fix progress indicator bug --- pkg/cmd/codespace/ports.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 4109488f9..f671c2449 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -309,11 +309,12 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar }) // wait for success or failure - if err := g.Wait(); err != nil { + err := g.Wait() + a.StopProgressIndicator() + if err != nil { return err } - a.StopProgressIndicator() } return nil From 9b1a6607ced0e47b3bca221e9db6e0e0c3bf4b64 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Mon, 7 Mar 2022 22:00:16 +0000 Subject: [PATCH 20/75] review suggestion: deregister handle after receiving notification --- pkg/liveshare/ports.go | 6 ++++-- pkg/liveshare/rpc.go | 25 ++++++++++++++++++++++++- pkg/liveshare/session.go | 6 +++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index b20e326a4..a1a9ffa39 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -91,8 +91,10 @@ func (s *Session) WaitForPortNotification(ctx context.Context, port int, notifTy notificationUpdate <- notification } } - s.registerRequestHandler("serverSharing.sharingSucceeded", h(true)) - s.registerRequestHandler("serverSharing.sharingFailed", h(false)) + deregisterSuccess := s.registerRequestHandler("serverSharing.sharingSucceeded", h(true)) + deregisterFailure := s.registerRequestHandler("serverSharing.sharingFailed", h(false)) + defer deregisterSuccess() + defer deregisterFailure() for { select { diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index a32d8507a..5972656aa 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -53,7 +53,7 @@ func newRequestHandler() *requestHandler { return &requestHandler{handlers: make(map[string][]handlerFn)} } -func (r *requestHandler) register(requestType string, handler handlerFn) { +func (r *requestHandler) register(requestType string, handler handlerFn) func() { r.handlersMu.Lock() defer r.handlersMu.Unlock() @@ -62,6 +62,29 @@ func (r *requestHandler) register(requestType string, handler handlerFn) { } r.handlers[requestType] = append(r.handlers[requestType], handler) + + return func() { + r.deregister(requestType, handler) + } +} + +func (r *requestHandler) deregister(requestType string, handler handlerFn) { + r.handlersMu.Lock() + defer r.handlersMu.Unlock() + + if handlers, ok := r.handlers[requestType]; ok { + newHandlers := []handlerFn{} + for _, h := range handlers { + if &h != &handler { + newHandlers = append(newHandlers, h) + } + } + r.handlers[requestType] = newHandlers + + if len(r.handlers[requestType]) == 0 { + delete(r.handlers, requestType) + } + } } func (r *requestHandler) handlerFn(requestType string) []handlerFn { diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index 6249acb23..c7e38225f 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -31,9 +31,9 @@ func (s *Session) Close() error { } // registerRequestHandler registers a handler for the given request type with the RPC -// server. -func (s *Session) registerRequestHandler(requestType string, h handlerFn) { - s.rpc.requestHandler.register(requestType, h) +// server and returns a callback function to deregister the handler +func (s *Session) registerRequestHandler(requestType string, h handlerFn) func() { + return s.rpc.requestHandler.register(requestType, h) } // StartsSSHServer starts an SSH server in the container, installing sshd if necessary, From a657fa808adab50f928c8caa126474b5940ad9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 8 Mar 2022 16:44:41 +0100 Subject: [PATCH 21/75] pr checks: fix error message when no checks were found Fixes the error message: no checks reported on the '' branch Now the correct branch name is reported. --- pkg/cmd/pr/checks/checks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/checks/checks.go b/pkg/cmd/pr/checks/checks.go index 37602e1e0..f9ae516c9 100644 --- a/pkg/cmd/pr/checks/checks.go +++ b/pkg/cmd/pr/checks/checks.go @@ -134,7 +134,7 @@ func checksRun(opts *ChecksOptions) error { for { findOptions := shared.FindOptions{ Selector: opts.SelectorArg, - Fields: []string{"number", "baseRefName", "statusCheckRollup"}, + Fields: []string{"number", "headRefName", "statusCheckRollup"}, } pr, _, err := opts.Finder.Find(findOptions) if err != nil { From e0045f26b9e28ca2769115c54d627c4f2fd89420 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Wed, 9 Mar 2022 14:24:27 +0200 Subject: [PATCH 22/75] Add top level search command and search repos sub command (#5172) --- pkg/cmd/root/root.go | 2 + pkg/cmd/run/list/list_test.go | 22 +-- pkg/cmd/search/repos/repos.go | 203 ++++++++++++++++++++ pkg/cmd/search/repos/repos_test.go | 295 +++++++++++++++++++++++++++++ pkg/cmd/search/search.go | 20 ++ pkg/cmdutil/flags.go | 55 +++++- pkg/httpmock/stub.go | 19 ++ pkg/search/query.go | 111 +++++++++++ pkg/search/query_test.go | 135 +++++++++++++ pkg/search/result.go | 120 ++++++++++++ pkg/search/result_test.go | 46 +++++ pkg/search/searcher.go | 184 ++++++++++++++++++ pkg/search/searcher_mock.go | 116 ++++++++++++ pkg/search/searcher_test.go | 198 +++++++++++++++++++ pkg/text/convert.go | 29 +++ pkg/text/convert_test.go | 61 ++++++ 16 files changed, 1588 insertions(+), 28 deletions(-) create mode 100644 pkg/cmd/search/repos/repos.go create mode 100644 pkg/cmd/search/repos/repos_test.go create mode 100644 pkg/cmd/search/search.go create mode 100644 pkg/search/query.go create mode 100644 pkg/search/query_test.go create mode 100644 pkg/search/result.go create mode 100644 pkg/search/result_test.go create mode 100644 pkg/search/searcher.go create mode 100644 pkg/search/searcher_mock.go create mode 100644 pkg/search/searcher_test.go create mode 100644 pkg/text/convert.go create mode 100644 pkg/text/convert_test.go diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 6ea865fe3..f77d17b55 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -25,6 +25,7 @@ import ( repoCmd "github.com/cli/cli/v2/pkg/cmd/repo" creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits" runCmd "github.com/cli/cli/v2/pkg/cmd/run" + searchCmd "github.com/cli/cli/v2/pkg/cmd/search" secretCmd "github.com/cli/cli/v2/pkg/cmd/secret" sshKeyCmd "github.com/cli/cli/v2/pkg/cmd/ssh-key" versionCmd "github.com/cli/cli/v2/pkg/cmd/version" @@ -79,6 +80,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(gpgKeyCmd.NewCmdGPGKey(f)) cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams)) cmd.AddCommand(extensionCmd.NewCmdExtension(f)) + cmd.AddCommand(searchCmd.NewCmdSearch(f)) cmd.AddCommand(secretCmd.NewCmdSecret(f)) cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f)) cmd.AddCommand(newCodespaceCmd(f)) diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index fb18d04d2..a3d2b06de 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -108,24 +108,6 @@ func TestNewCmdList(t *testing.T) { } func TestListRun(t *testing.T) { - // helper to match mocked requests by their query params along with method and path - queryMatcher := func(method string, path string, query url.Values) httpmock.Matcher { - return func(req *http.Request) bool { - if !httpmock.REST(method, path)(req) { - return false - } - - actualQuery := req.URL.Query() - - for param := range query { - if !(actualQuery.Get(param) == query.Get(param)) { - return false - } - } - - return true - } - } tests := []struct { name string opts *ListOptions @@ -244,7 +226,7 @@ func TestListRun(t *testing.T) { }, stubs: func(reg *httpmock.Registry) { reg.Register( - queryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ + httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "branch": []string{"the-branch"}, }), httpmock.JSONResponse(shared.RunsPayload{}), @@ -261,7 +243,7 @@ func TestListRun(t *testing.T) { }, stubs: func(reg *httpmock.Registry) { reg.Register( - queryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ + httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "actor": []string{"bak1an"}, }), httpmock.JSONResponse(shared.RunsPayload{}), diff --git a/pkg/cmd/search/repos/repos.go b/pkg/cmd/search/repos/repos.go new file mode 100644 index 000000000..878599dc6 --- /dev/null +++ b/pkg/cmd/search/repos/repos.go @@ -0,0 +1,203 @@ +package repos + +import ( + "fmt" + "strings" + "time" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/search" + "github.com/cli/cli/v2/pkg/text" + "github.com/cli/cli/v2/utils" + "github.com/spf13/cobra" +) + +const ( + // Limitation of GitHub search see: + // https://docs.github.com/en/rest/reference/search + searchMaxResults = 1000 +) + +type ReposOptions struct { + Browser cmdutil.Browser + Exporter cmdutil.Exporter + IO *iostreams.IOStreams + Query search.Query + Searcher search.Searcher + WebMode bool +} + +func NewCmdRepos(f *cmdutil.Factory, runF func(*ReposOptions) error) *cobra.Command { + var order string + var sort string + opts := &ReposOptions{ + Browser: f.Browser, + IO: f.IOStreams, + Query: search.Query{Kind: search.KindRepositories}, + } + + cmd := &cobra.Command{ + Use: "repos []", + Short: "Search for repositories", + Long: heredoc.Doc(` + Search for repositories on GitHub. + + The command supports constructing queries using the GitHub search syntax, + using the parameter and qualifier flags, or a combination of the two. + + GitHub search syntax is documented at: + https://docs.github.com/search-github/searching-on-github/searching-for-repositories + `), + Example: heredoc.Doc(` + # search repositories matching set of keywords "cli" and "shell" + $ gh search repos cli shell + + # search repositories matching phrase "vim plugin" + $ gh search repos "vim plugin" + + # search repositories public repos in the microsoft organization + $ gh search repos --owner=microsoft --visibility=public + + # search repositories with a set of topics + $ gh search repos --topic=unix,terminal + + # search repositories by coding language and number of good first issues + $ gh search repos --language=go --good-first-issues=">=10" + `), + RunE: func(c *cobra.Command, args []string) error { + if len(args) == 0 && c.Flags().NFlag() == 0 { + return cmdutil.FlagErrorf("specify search keywords or flags") + } + if opts.Query.Limit < 1 || opts.Query.Limit > searchMaxResults { + return cmdutil.FlagErrorf("`--limit` must be between 1 and 1000") + } + if c.Flags().Changed("order") { + opts.Query.Order = order + } + if c.Flags().Changed("sort") { + opts.Query.Sort = sort + } + opts.Query.Keywords = args + if runF != nil { + return runF(opts) + } + var err error + opts.Searcher, err = searcher(f) + if err != nil { + return err + } + return reposRun(opts) + }, + } + + // Output flags + cmdutil.AddJSONFlags(cmd, &opts.Exporter, search.RepositoryFields) + cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the search query in the web browser") + + // Query parameter flags + cmd.Flags().IntVarP(&opts.Query.Limit, "limit", "L", 30, "Maximum number of repositories to fetch") + cmdutil.StringEnumFlag(cmd, &order, "order", "", "desc", []string{"asc", "desc"}, "Order of repositories returned, ignored unless '--sort' flag is specified") + cmdutil.StringEnumFlag(cmd, &sort, "sort", "", "best-match", []string{"forks", "help-wanted-issues", "stars", "updated"}, "Sort fetched repositories") + + // Query qualifier flags + cmdutil.NilBoolFlag(cmd, &opts.Query.Qualifiers.Archived, "archived", "", "Filter based on archive state") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Created, "created", "", "Filter based on created at `date`") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Followers, "followers", "", "Filter based on `number` of followers") + cmdutil.StringEnumFlag(cmd, &opts.Query.Qualifiers.Fork, "include-forks", "", "", []string{"false", "true", "only"}, "Include forks in fetched repositories") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Forks, "forks", "", "Filter on `number` of forks") + cmd.Flags().StringVar(&opts.Query.Qualifiers.GoodFirstIssues, "good-first-issues", "", "Filter on `number` of issues with the 'good first issue' label") + cmd.Flags().StringVar(&opts.Query.Qualifiers.HelpWantedIssues, "help-wanted-issues", "", "Filter on `number` of issues with the 'help wanted' label") + cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.In, "match", "", nil, []string{"name", "description", "readme"}, "Restrict search to specific field of repository") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Language, "language", "", "Filter based on the coding language") + cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.License, "license", nil, "Filter based on license type") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Org, "owner", "", "Filter on owner") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Pushed, "updated", "", "Filter on last updated at `date`") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Size, "size", "", "Filter on a size range, in kilobytes") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Stars, "stars", "", "Filter on `number` of stars") + cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.Topic, "topic", nil, "Filter on topic") + cmd.Flags().StringVar(&opts.Query.Qualifiers.Topics, "number-topics", "", "Filter on `number` of topics") + cmdutil.StringEnumFlag(cmd, &opts.Query.Qualifiers.Is, "visibility", "", "", []string{"public", "private", "internal"}, "Filter based on visibility") + + return cmd +} + +func reposRun(opts *ReposOptions) error { + io := opts.IO + if opts.WebMode { + url := opts.Searcher.URL(opts.Query) + if io.IsStdoutTTY() { + fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(url)) + } + return opts.Browser.Browse(url) + } + io.StartProgressIndicator() + result, err := opts.Searcher.Repositories(opts.Query) + io.StopProgressIndicator() + if err != nil { + return err + } + if err := io.StartPager(); err == nil { + defer io.StopPager() + } else { + fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err) + } + if opts.Exporter != nil { + return opts.Exporter.Write(io, result.Items) + } + return displayResults(io, result) +} + +func displayResults(io *iostreams.IOStreams, results search.RepositoriesResult) error { + cs := io.ColorScheme() + tp := utils.NewTablePrinter(io) + for _, repo := range results.Items { + tags := []string{repo.Visibility} + if repo.IsFork { + tags = append(tags, "fork") + } + if repo.IsArchived { + tags = append(tags, "archived") + } + info := strings.Join(tags, ", ") + infoColor := cs.Gray + if repo.IsPrivate { + infoColor = cs.Yellow + } + tp.AddField(repo.FullName, nil, cs.Bold) + description := repo.Description + tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil) + tp.AddField(info, nil, infoColor) + if tp.IsTTY() { + tp.AddField(utils.FuzzyAgoAbbr(time.Now(), repo.UpdatedAt), nil, cs.Gray) + } else { + tp.AddField(repo.UpdatedAt.Format(time.RFC3339), nil, nil) + } + tp.EndRow() + } + if io.IsStdoutTTY() { + header := "No repositories matched your search\n" + if len(results.Items) > 0 { + header = fmt.Sprintf("Showing %d of %d repositories\n\n", len(results.Items), results.Total) + } + fmt.Fprintf(io.Out, "\n%s", header) + } + return tp.Render() +} + +func searcher(f *cmdutil.Factory) (search.Searcher, error) { + cfg, err := f.Config() + if err != nil { + return nil, err + } + host, err := cfg.DefaultHost() + if err != nil { + return nil, err + } + client, err := f.HttpClient() + if err != nil { + return nil, err + } + return search.NewSearcher(client, host), nil +} diff --git a/pkg/cmd/search/repos/repos_test.go b/pkg/cmd/search/repos/repos_test.go new file mode 100644 index 000000000..872171b01 --- /dev/null +++ b/pkg/cmd/search/repos/repos_test.go @@ -0,0 +1,295 @@ +package repos + +import ( + "bytes" + "fmt" + "testing" + "time" + + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/search" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdRepos(t *testing.T) { + var trueBool = true + tests := []struct { + name string + input string + output ReposOptions + wantErr bool + errMsg string + }{ + { + name: "no arguments", + input: "", + wantErr: true, + errMsg: "specify search keywords or flags", + }, + { + name: "keyword arguments", + input: "some search terms", + output: ReposOptions{ + Query: search.Query{Keywords: []string{"some", "search", "terms"}, Kind: "repositories", Limit: 30}, + }, + }, + { + name: "web flag", + input: "--web", + output: ReposOptions{ + Query: search.Query{Keywords: []string{}, Kind: "repositories", Limit: 30}, + WebMode: true, + }, + }, + { + name: "limit flag", + input: "--limit 10", + output: ReposOptions{Query: search.Query{Keywords: []string{}, Kind: "repositories", Limit: 10}}, + }, + { + name: "invalid limit flag", + input: "--limit 1001", + wantErr: true, + errMsg: "`--limit` must be between 1 and 1000", + }, + { + name: "order flag", + input: "--order asc", + output: ReposOptions{ + Query: search.Query{Keywords: []string{}, Kind: "repositories", Limit: 30, Order: "asc"}, + }, + }, + { + name: "invalid order flag", + input: "--order invalid", + wantErr: true, + errMsg: "invalid argument \"invalid\" for \"--order\" flag: valid values are {asc|desc}", + }, + { + name: "qualifier flags", + input: ` + --archived + --created=created + --followers=1 + --include-forks=true + --forks=2 + --good-first-issues=3 + --help-wanted-issues=4 + --match=description,readme + --language=language + --license=license + --owner=owner + --updated=updated + --size=5 + --stars=6 + --topic=topic + --number-topics=7 + --visibility=public + `, + output: ReposOptions{ + Query: search.Query{ + Keywords: []string{}, + Kind: "repositories", + Limit: 30, + Qualifiers: search.Qualifiers{ + Archived: &trueBool, + Created: "created", + Followers: "1", + Fork: "true", + Forks: "2", + GoodFirstIssues: "3", + HelpWantedIssues: "4", + In: []string{"description", "readme"}, + Language: "language", + License: []string{"license"}, + Org: "owner", + Pushed: "updated", + Size: "5", + Stars: "6", + Topic: []string{"topic"}, + Topics: "7", + Is: "public", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: io, + } + argv, err := shlex.Split(tt.input) + assert.NoError(t, err) + var gotOpts *ReposOptions + cmd := NewCmdRepos(f, func(opts *ReposOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.output.Query, gotOpts.Query) + assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) + }) + } +} + +func Test_ReposRun(t *testing.T) { + var query = search.Query{ + Keywords: []string{"cli"}, + Kind: "repositories", + Limit: 30, + Qualifiers: search.Qualifiers{ + Stars: ">50", + Topic: []string{"golang"}, + }, + } + var updatedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC) + tests := []struct { + errMsg string + name string + opts *ReposOptions + tty bool + wantErr bool + wantStderr string + wantStdout string + }{ + { + name: "displays results tty", + opts: &ReposOptions{ + Query: query, + Searcher: &search.SearcherMock{ + RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) { + return search.RepositoriesResult{ + IncompleteResults: false, + Items: []search.Repository{ + {FullName: "test/cli", Description: "of course", IsPrivate: true, IsArchived: true, UpdatedAt: updatedAt, Visibility: "private"}, + {FullName: "test/cliing", Description: "wow", IsFork: true, UpdatedAt: updatedAt, Visibility: "public"}, + {FullName: "cli/cli", Description: "so much", IsArchived: false, UpdatedAt: updatedAt, Visibility: "internal"}, + }, + Total: 300, + }, nil + }, + }, + }, + tty: true, + wantStdout: "\nShowing 3 of 300 repositories\n\ntest/cli of course private, archived Feb 28, 2021\ntest/cliing wow public, fork Feb 28, 2021\ncli/cli so much internal Feb 28, 2021\n", + }, + { + name: "displays no results tty", + opts: &ReposOptions{ + Query: query, + Searcher: &search.SearcherMock{ + RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) { + return search.RepositoriesResult{}, nil + }, + }, + }, + tty: true, + wantStdout: "\nNo repositories matched your search\n", + }, + { + name: "displays results notty", + opts: &ReposOptions{ + Query: query, + Searcher: &search.SearcherMock{ + RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) { + return search.RepositoriesResult{ + IncompleteResults: false, + Items: []search.Repository{ + {FullName: "test/cli", Description: "of course", IsPrivate: true, IsArchived: true, UpdatedAt: updatedAt, Visibility: "private"}, + {FullName: "test/cliing", Description: "wow", IsFork: true, UpdatedAt: updatedAt, Visibility: "public"}, + {FullName: "cli/cli", Description: "so much", IsArchived: false, UpdatedAt: updatedAt, Visibility: "internal"}, + }, + Total: 300, + }, nil + }, + }, + }, + wantStdout: "test/cli\tof course\tprivate, archived\t2021-02-28T12:30:00Z\ntest/cliing\twow\tpublic, fork\t2021-02-28T12:30:00Z\ncli/cli\tso much\tinternal\t2021-02-28T12:30:00Z\n", + }, + { + name: "displays no results notty", + opts: &ReposOptions{ + Query: query, + Searcher: &search.SearcherMock{ + RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) { + return search.RepositoriesResult{}, nil + }, + }, + }, + }, + { + name: "displays search error", + opts: &ReposOptions{ + Query: query, + Searcher: &search.SearcherMock{ + RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) { + return search.RepositoriesResult{}, fmt.Errorf("error with query") + }, + }, + }, + errMsg: "error with query", + wantErr: true, + }, + { + name: "opens browser for web mode tty", + opts: &ReposOptions{ + Browser: &cmdutil.TestBrowser{}, + Query: query, + Searcher: &search.SearcherMock{ + URLFunc: func(query search.Query) string { + return "https://github.com/search?type=repositories&q=cli" + }, + }, + WebMode: true, + }, + tty: true, + wantStderr: "Opening github.com/search in your browser.\n", + }, + { + name: "opens browser for web mode notty", + opts: &ReposOptions{ + Browser: &cmdutil.TestBrowser{}, + Query: query, + Searcher: &search.SearcherMock{ + URLFunc: func(query search.Query) string { + return "https://github.com/search?type=repositories&q=cli" + }, + }, + WebMode: true, + }, + }, + } + for _, tt := range tests { + io, _, stdout, stderr := iostreams.Test() + io.SetStdinTTY(tt.tty) + io.SetStdoutTTY(tt.tty) + io.SetStderrTTY(tt.tty) + tt.opts.IO = io + t.Run(tt.name, func(t *testing.T) { + err := reposRun(tt.opts) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } else if err != nil { + t.Fatalf("reposRun unexpected error: %v", err) + } + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) + }) + } +} diff --git a/pkg/cmd/search/search.go b/pkg/cmd/search/search.go new file mode 100644 index 000000000..5342175c8 --- /dev/null +++ b/pkg/cmd/search/search.go @@ -0,0 +1,20 @@ +package search + +import ( + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" + + searchReposCmd "github.com/cli/cli/v2/pkg/cmd/search/repos" +) + +func NewCmdSearch(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "search ", + Short: "Search for repositories, issues, pull requests and users", + Long: "Search across all of GitHub.", + } + + cmd.AddCommand(searchReposCmd.NewCmdRepos(f, nil)) + + return cmd +} diff --git a/pkg/cmdutil/flags.go b/pkg/cmdutil/flags.go index dee70069e..2a73a79de 100644 --- a/pkg/cmdutil/flags.go +++ b/pkg/cmdutil/flags.go @@ -34,6 +34,16 @@ func StringEnumFlag(cmd *cobra.Command, p *string, name, shorthand, defaultValue return f } +func StringSliceEnumFlag(cmd *cobra.Command, p *[]string, name, shorthand string, defaultValues, options []string, usage string) *pflag.Flag { + *p = defaultValues + val := &enumMultiValue{value: p, options: options} + f := cmd.Flags().VarPF(val, name, shorthand, fmt.Sprintf("%s: %s", usage, formatValuesForUsageDocs(options))) + _ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return options, cobra.ShellCompDirectiveNoFileComp + }) + return f +} + func formatValuesForUsageDocs(values []string) string { return fmt.Sprintf("{%s}", strings.Join(values, "|")) } @@ -99,14 +109,7 @@ type enumValue struct { } func (e *enumValue) Set(value string) error { - found := false - for _, opt := range e.options { - if strings.EqualFold(opt, value) { - found = true - break - } - } - if !found { + if !isIncluded(value, e.options) { return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options)) } *e.string = value @@ -120,3 +123,39 @@ func (e *enumValue) String() string { func (e *enumValue) Type() string { return "string" } + +type enumMultiValue struct { + value *[]string + options []string +} + +func (e *enumMultiValue) Set(value string) error { + items := strings.Split(value, ",") + for _, item := range items { + if !isIncluded(item, e.options) { + return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options)) + } + } + *e.value = append(*e.value, items...) + return nil +} + +func (e *enumMultiValue) String() string { + if len(*e.value) == 0 { + return "" + } + return fmt.Sprintf("{%s}", strings.Join(*e.value, ", ")) +} + +func (e *enumMultiValue) Type() string { + return "stringSlice" +} + +func isIncluded(value string, opts []string) bool { + for _, opt := range opts { + if strings.EqualFold(opt, value) { + return true + } + } + return false +} diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 5633c2caf..8970e8f49 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "regexp" "strings" @@ -56,6 +57,24 @@ func GraphQL(q string) Matcher { } } +func QueryMatcher(method string, path string, query url.Values) Matcher { + return func(req *http.Request) bool { + if !REST(method, path)(req) { + return false + } + + actualQuery := req.URL.Query() + + for param := range query { + if !(actualQuery.Get(param) == query.Get(param)) { + return false + } + } + + return true + } +} + func readBody(req *http.Request) ([]byte, error) { bodyCopy := &bytes.Buffer{} r := io.TeeReader(req.Body, bodyCopy) diff --git a/pkg/search/query.go b/pkg/search/query.go new file mode 100644 index 000000000..a0966ba93 --- /dev/null +++ b/pkg/search/query.go @@ -0,0 +1,111 @@ +package search + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/cli/cli/v2/pkg/text" +) + +const ( + KindRepositories = "repositories" +) + +type Query struct { + Keywords []string + Kind string + Limit int + Order string + Page int + Qualifiers Qualifiers + Sort string +} + +type Qualifiers struct { + Archived *bool + Created string + Followers string + Fork string + Forks string + GoodFirstIssues string + HelpWantedIssues string + In []string + Is string + Language string + License []string + Org string + Pushed string + Size string + Stars string + Topic []string + Topics string +} + +func (q Query) String() string { + qualifiers := formatQualifiers(q.Qualifiers) + keywords := formatKeywords(q.Keywords) + all := append(keywords, qualifiers...) + return strings.Join(all, " ") +} + +func (q Qualifiers) Map() map[string][]string { + m := map[string][]string{} + v := reflect.ValueOf(q) + t := reflect.TypeOf(q) + for i := 0; i < v.NumField(); i++ { + fieldName := t.Field(i).Name + key := text.CamelToKebab(fieldName) + typ := v.FieldByName(fieldName).Kind() + value := v.FieldByName(fieldName) + switch typ { + case reflect.Ptr: + if value.IsNil() { + continue + } + v := reflect.Indirect(value) + m[key] = []string{fmt.Sprintf("%v", v)} + case reflect.Slice: + if value.IsNil() { + continue + } + s := []string{} + for i := 0; i < value.Len(); i++ { + s = append(s, fmt.Sprintf("%v", value.Index(i))) + } + m[key] = s + default: + if value.IsZero() { + continue + } + m[key] = []string{fmt.Sprintf("%v", value)} + } + } + return m +} + +func quote(s string) string { + if strings.ContainsAny(s, " \"\t\r\n") { + return fmt.Sprintf("%q", s) + } + return s +} + +func formatQualifiers(qs Qualifiers) []string { + var all []string + for k, vs := range qs.Map() { + for _, v := range vs { + all = append(all, fmt.Sprintf("%s:%s", k, quote(v))) + } + } + sort.Strings(all) + return all +} + +func formatKeywords(ks []string) []string { + for i, k := range ks { + ks[i] = quote(k) + } + return ks +} diff --git a/pkg/search/query_test.go b/pkg/search/query_test.go new file mode 100644 index 000000000..889acae76 --- /dev/null +++ b/pkg/search/query_test.go @@ -0,0 +1,135 @@ +package search + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var trueBool = true + +func TestQueryString(t *testing.T) { + tests := []struct { + name string + query Query + out string + }{ + { + name: "converts query to string", + query: Query{ + Keywords: []string{"some", "keywords"}, + Qualifiers: Qualifiers{ + Archived: &trueBool, + Created: "created", + Followers: "1", + Fork: "true", + Forks: "2", + GoodFirstIssues: "3", + HelpWantedIssues: "4", + In: []string{"description", "readme"}, + Language: "language", + License: []string{"license"}, + Org: "org", + Pushed: "updated", + Size: "5", + Stars: "6", + Topic: []string{"topic"}, + Topics: "7", + Is: "public", + }, + }, + out: "some keywords archived:true created:created followers:1 fork:true forks:2 good-first-issues:3 help-wanted-issues:4 in:description in:readme is:public language:language license:license org:org pushed:updated size:5 stars:6 topic:topic topics:7", + }, + { + name: "quotes keywords", + query: Query{ + Keywords: []string{"quote keywords"}, + }, + out: "\"quote keywords\"", + }, + { + name: "quotes qualifiers", + query: Query{ + Qualifiers: Qualifiers{ + Topic: []string{"quote qualifier"}, + }, + }, + out: "topic:\"quote qualifier\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.out, tt.query.String()) + }) + } +} + +func TestQualifiersMap(t *testing.T) { + tests := []struct { + name string + qualifiers Qualifiers + out map[string][]string + }{ + { + name: "changes qualifiers to map", + qualifiers: Qualifiers{ + Archived: &trueBool, + Created: "created", + Followers: "1", + Fork: "true", + Forks: "2", + GoodFirstIssues: "3", + HelpWantedIssues: "4", + In: []string{"readme"}, + Language: "language", + License: []string{"license"}, + Org: "org", + Pushed: "updated", + Size: "5", + Stars: "6", + Topic: []string{"topic"}, + Topics: "7", + Is: "public", + }, + out: map[string][]string{ + "archived": {"true"}, + "created": {"created"}, + "followers": {"1"}, + "fork": {"true"}, + "forks": {"2"}, + "good-first-issues": {"3"}, + "help-wanted-issues": {"4"}, + "in": {"readme"}, + "is": {"public"}, + "language": {"language"}, + "license": {"license"}, + "org": {"org"}, + "pushed": {"updated"}, + "size": {"5"}, + "stars": {"6"}, + "topic": {"topic"}, + "topics": {"7"}, + }, + }, + { + name: "excludes unset qualifiers from map", + qualifiers: Qualifiers{ + Org: "org", + Pushed: "updated", + Size: "5", + Stars: "6", + }, + out: map[string][]string{ + "org": {"org"}, + "pushed": {"updated"}, + "size": {"5"}, + "stars": {"6"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.out, tt.qualifiers.Map()) + }) + } +} diff --git a/pkg/search/result.go b/pkg/search/result.go new file mode 100644 index 000000000..99b3d2142 --- /dev/null +++ b/pkg/search/result.go @@ -0,0 +1,120 @@ +package search + +import ( + "reflect" + "strings" + "time" +) + +var RepositoryFields = []string{ + "createdAt", + "defaultBranch", + "description", + "forksCount", + "fullName", + "hasDownloads", + "hasIssues", + "hasPages", + "hasProjects", + "hasWiki", + "homepage", + "id", + "isArchived", + "isDisabled", + "isFork", + "isPrivate", + "language", + "license", + "name", + "openIssuesCount", + "owner", + "pushedAt", + "size", + "stargazersCount", + "updatedAt", + "visibility", + "watchersCount", +} + +type RepositoriesResult struct { + IncompleteResults bool `json:"incomplete_results"` + Items []Repository `json:"items"` + Total int `json:"total_count"` +} + +type Repository struct { + CreatedAt time.Time `json:"created_at"` + DefaultBranch string `json:"default_branch"` + Description string `json:"description"` + ForksCount int `json:"forks_count"` + FullName string `json:"full_name"` + HasDownloads bool `json:"has_downloads"` + HasIssues bool `json:"has_issues"` + HasPages bool `json:"has_pages"` + HasProjects bool `json:"has_projects"` + HasWiki bool `json:"has_wiki"` + Homepage string `json:"homepage"` + ID int64 `json:"id"` + IsArchived bool `json:"archived"` + IsDisabled bool `json:"disabled"` + IsFork bool `json:"fork"` + IsPrivate bool `json:"private"` + Language string `json:"language"` + License License `json:"license"` + MasterBranch string `json:"master_branch"` + Name string `json:"name"` + OpenIssuesCount int `json:"open_issues_count"` + Owner User `json:"owner"` + PushedAt time.Time `json:"pushed_at"` + Size int `json:"size"` + StargazersCount int `json:"stargazers_count"` + UpdatedAt time.Time `json:"updated_at"` + Visibility string `json:"visibility"` + WatchersCount int `json:"watchers_count"` +} + +type License struct { + HTMLURL string `json:"html_url"` + Key string `json:"key"` + Name string `json:"name"` + URL string `json:"url"` +} + +type User struct { + GravatarID string `json:"gravatar_id"` + ID int64 `json:"id"` + Login string `json:"login"` + SiteAdmin bool `json:"site_admin"` + Type string `json:"type"` +} + +func (repo Repository) ExportData(fields []string) map[string]interface{} { + v := reflect.ValueOf(repo) + data := map[string]interface{}{} + for _, f := range fields { + switch f { + case "license": + data[f] = map[string]interface{}{ + "key": repo.License.Key, + "name": repo.License.Name, + "url": repo.License.URL, + } + case "owner": + data[f] = map[string]interface{}{ + "id": repo.Owner.ID, + "login": repo.Owner.Login, + "type": repo.Owner.Type, + } + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + return data +} + +func fieldByName(v reflect.Value, field string) reflect.Value { + return v.FieldByNameFunc(func(s string) bool { + return strings.EqualFold(field, s) + }) +} diff --git a/pkg/search/result_test.go b/pkg/search/result_test.go new file mode 100644 index 000000000..185cc4e36 --- /dev/null +++ b/pkg/search/result_test.go @@ -0,0 +1,46 @@ +package search + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepositoryExportData(t *testing.T) { + var createdAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC) + tests := []struct { + name string + fields []string + repo Repository + output string + }{ + { + name: "exports requested fields", + fields: []string{"createdAt", "description", "fullName", "isArchived", "isFork", "isPrivate", "pushedAt"}, + repo: Repository{ + CreatedAt: createdAt, + Description: "description", + FullName: "cli/cli", + IsArchived: true, + IsFork: false, + IsPrivate: false, + PushedAt: createdAt, + }, + output: `{"createdAt":"2021-02-28T12:30:00Z","description":"description","fullName":"cli/cli","isArchived":true,"isFork":false,"isPrivate":false,"pushedAt":"2021-02-28T12:30:00Z"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exported := tt.repo.ExportData(tt.fields) + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + require.NoError(t, enc.Encode(exported)) + assert.Equal(t, tt.output, strings.TrimSpace(buf.String())) + }) + } +} diff --git a/pkg/search/searcher.go b/pkg/search/searcher.go new file mode 100644 index 000000000..39d2c09c5 --- /dev/null +++ b/pkg/search/searcher.go @@ -0,0 +1,184 @@ +package search + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/cli/cli/v2/internal/ghinstance" +) + +const ( + maxPerPage = 100 + orderKey = "order" + sortKey = "sort" +) + +var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) +var pageRE = regexp.MustCompile(`(\?|&)page=(\d*)`) +var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) + +//go:generate moq -rm -out searcher_mock.go . Searcher +type Searcher interface { + Repositories(Query) (RepositoriesResult, error) + URL(Query) string +} + +type searcher struct { + client *http.Client + host string +} + +type httpError struct { + Errors []httpErrorItem + Message string + RequestURL *url.URL + StatusCode int +} + +type httpErrorItem struct { + Code string + Field string + Message string + Resource string +} + +func NewSearcher(client *http.Client, host string) Searcher { + return &searcher{ + client: client, + host: host, + } +} + +func (s searcher) Repositories(query Query) (RepositoriesResult, error) { + result := RepositoriesResult{} + toRetrieve := query.Limit + var resp *http.Response + var err error + for toRetrieve > 0 { + query.Limit = min(toRetrieve, maxPerPage) + query.Page = nextPage(resp) + if query.Page == 0 { + break + } + page := RepositoriesResult{} + resp, err = s.search(query, &page) + if err != nil { + return result, err + } + result.IncompleteResults = page.IncompleteResults + result.Total = page.Total + result.Items = append(result.Items, page.Items...) + toRetrieve = toRetrieve - len(page.Items) + } + return result, nil +} + +func (s searcher) search(query Query, result interface{}) (*http.Response, error) { + path := fmt.Sprintf("%ssearch/%s", ghinstance.RESTPrefix(s.host), query.Kind) + qs := url.Values{} + qs.Set("page", strconv.Itoa(query.Page)) + qs.Set("per_page", strconv.Itoa(query.Limit)) + qs.Set("q", query.String()) + if query.Order != "" { + qs.Set(orderKey, query.Order) + } + if query.Sort != "" { + qs.Set(sortKey, query.Sort) + } + url := fmt.Sprintf("%s?%s", path, qs.Encode()) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Accept", "application/vnd.github.v3+json") + resp, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + return resp, handleHTTPError(resp) + } + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(result) + if err != nil { + return resp, err + } + return resp, nil +} + +func (s searcher) URL(query Query) string { + path := fmt.Sprintf("https://%s/search", s.host) + qs := url.Values{} + qs.Set("type", query.Kind) + qs.Set("q", query.String()) + if query.Order != "" { + qs.Set(orderKey, query.Order) + } + if query.Sort != "" { + qs.Set(sortKey, query.Sort) + } + url := fmt.Sprintf("%s?%s", path, qs.Encode()) + return url +} + +func (err httpError) Error() string { + if err.StatusCode != 422 || len(err.Errors) == 0 { + return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL) + } + query := strings.TrimSpace(err.RequestURL.Query().Get("q")) + return fmt.Sprintf("Invalid search query %q.\n%s", query, err.Errors[0].Message) +} + +func handleHTTPError(resp *http.Response) error { + httpError := httpError{ + RequestURL: resp.Request.URL, + StatusCode: resp.StatusCode, + } + if !jsonTypeRE.MatchString(resp.Header.Get("Content-Type")) { + httpError.Message = resp.Status + return httpError + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if err := json.Unmarshal(body, &httpError); err != nil { + return err + } + return httpError +} + +func nextPage(resp *http.Response) (page int) { + if resp == nil { + return 1 + } + for _, m := range linkRE.FindAllStringSubmatch(resp.Header.Get("Link"), -1) { + if !(len(m) > 2 && m[2] == "next") { + continue + } + p := pageRE.FindStringSubmatch(m[1]) + if len(p) == 3 { + i, err := strconv.Atoi(p[2]) + if err == nil { + return i + } + } + } + return 0 +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/pkg/search/searcher_mock.go b/pkg/search/searcher_mock.go new file mode 100644 index 000000000..9d584867b --- /dev/null +++ b/pkg/search/searcher_mock.go @@ -0,0 +1,116 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package search + +import ( + "sync" +) + +// Ensure, that SearcherMock does implement Searcher. +// If this is not the case, regenerate this file with moq. +var _ Searcher = &SearcherMock{} + +// SearcherMock is a mock implementation of Searcher. +// +// func TestSomethingThatUsesSearcher(t *testing.T) { +// +// // make and configure a mocked Searcher +// mockedSearcher := &SearcherMock{ +// RepositoriesFunc: func(query Query) (RepositoriesResult, error) { +// panic("mock out the Repositories method") +// }, +// URLFunc: func(query Query) string { +// panic("mock out the URL method") +// }, +// } +// +// // use mockedSearcher in code that requires Searcher +// // and then make assertions. +// +// } +type SearcherMock struct { + // RepositoriesFunc mocks the Repositories method. + RepositoriesFunc func(query Query) (RepositoriesResult, error) + + // URLFunc mocks the URL method. + URLFunc func(query Query) string + + // calls tracks calls to the methods. + calls struct { + // Repositories holds details about calls to the Repositories method. + Repositories []struct { + // Query is the query argument value. + Query Query + } + // URL holds details about calls to the URL method. + URL []struct { + // Query is the query argument value. + Query Query + } + } + lockRepositories sync.RWMutex + lockURL sync.RWMutex +} + +// Repositories calls RepositoriesFunc. +func (mock *SearcherMock) Repositories(query Query) (RepositoriesResult, error) { + if mock.RepositoriesFunc == nil { + panic("SearcherMock.RepositoriesFunc: method is nil but Searcher.Repositories was just called") + } + callInfo := struct { + Query Query + }{ + Query: query, + } + mock.lockRepositories.Lock() + mock.calls.Repositories = append(mock.calls.Repositories, callInfo) + mock.lockRepositories.Unlock() + return mock.RepositoriesFunc(query) +} + +// RepositoriesCalls gets all the calls that were made to Repositories. +// Check the length with: +// len(mockedSearcher.RepositoriesCalls()) +func (mock *SearcherMock) RepositoriesCalls() []struct { + Query Query +} { + var calls []struct { + Query Query + } + mock.lockRepositories.RLock() + calls = mock.calls.Repositories + mock.lockRepositories.RUnlock() + return calls +} + +// URL calls URLFunc. +func (mock *SearcherMock) URL(query Query) string { + if mock.URLFunc == nil { + panic("SearcherMock.URLFunc: method is nil but Searcher.URL was just called") + } + callInfo := struct { + Query Query + }{ + Query: query, + } + mock.lockURL.Lock() + mock.calls.URL = append(mock.calls.URL, callInfo) + mock.lockURL.Unlock() + return mock.URLFunc(query) +} + +// URLCalls gets all the calls that were made to URL. +// Check the length with: +// len(mockedSearcher.URLCalls()) +func (mock *SearcherMock) URLCalls() []struct { + Query Query +} { + var calls []struct { + Query Query + } + mock.lockURL.RLock() + calls = mock.calls.URL + mock.lockURL.RUnlock() + return calls +} diff --git a/pkg/search/searcher_test.go b/pkg/search/searcher_test.go new file mode 100644 index 000000000..9aadc32f1 --- /dev/null +++ b/pkg/search/searcher_test.go @@ -0,0 +1,198 @@ +package search + +import ( + "net/http" + "net/url" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" +) + +var query = Query{ + Keywords: []string{"keyword"}, + Kind: "repositories", + Limit: 30, + Order: "stars", + Sort: "desc", + Qualifiers: Qualifiers{ + Stars: ">=5", + Topic: []string{"topic"}, + }, +} + +func TestSearcherRepositories(t *testing.T) { + values := url.Values{ + "page": []string{"1"}, + "per_page": []string{"30"}, + "order": []string{"stars"}, + "sort": []string{"desc"}, + "q": []string{"keyword stars:>=5 topic:topic"}, + } + + tests := []struct { + name string + host string + query Query + result RepositoriesResult + wantErr bool + errMsg string + httpStubs func(*httpmock.Registry) + }{ + { + name: "searches repositories", + query: query, + result: RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }), + ) + }, + }, + { + name: "searches repositories for enterprise host", + host: "enterprise.com", + query: query, + result: RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("GET", "api/v3/search/repositories", values), + httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }), + ) + }, + }, + { + name: "paginates results", + query: query, + result: RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}, {Name: "cli"}}, + Total: 2, + }, + httpStubs: func(reg *httpmock.Registry) { + firstReq := httpmock.QueryMatcher("GET", "search/repositories", values) + firstRes := httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 2, + }, + ) + firstRes = httpmock.WithHeader(firstRes, "Link", `; rel="next"`) + secondReq := httpmock.QueryMatcher("GET", "search/repositories", url.Values{ + "page": []string{"2"}, + "per_page": []string{"29"}, + "order": []string{"stars"}, + "sort": []string{"desc"}, + "q": []string{"keyword stars:>=5 topic:topic"}, + }, + ) + secondRes := httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "cli"}}, + Total: 2, + }, + ) + reg.Register(firstReq, firstRes) + reg.Register(secondReq, secondRes) + }, + }, + { + name: "handles search errors", + query: query, + wantErr: true, + errMsg: heredoc.Doc(` + Invalid search query "keyword stars:>=5 topic:topic". + "blah" is not a recognized date/time format. Please provide an ISO 8601 date/time value, such as YYYY-MM-DD.`), + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.WithHeader( + httpmock.StatusStringResponse(422, + `{ + "message":"Validation Failed", + "errors":[ + { + "message":"\"blah\" is not a recognized date/time format. Please provide an ISO 8601 date/time value, such as YYYY-MM-DD.", + "resource":"Search", + "field":"q", + "code":"invalid" + } + ], + "documentation_url":"https://docs.github.com/v3/search/" + }`, + ), "Content-Type", "application/json"), + ) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + client := &http.Client{Transport: reg} + if tt.host == "" { + tt.host = "github.com" + } + searcher := NewSearcher(client, tt.host) + result, err := searcher.Repositories(tt.query) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.result, result) + }) + } +} + +func TestSearcherURL(t *testing.T) { + tests := []struct { + name string + host string + query Query + url string + }{ + { + name: "outputs encoded query url", + query: query, + url: "https://github.com/search?order=stars&q=keyword+stars%3A%3E%3D5+topic%3Atopic&sort=desc&type=repositories", + }, + { + name: "supports enterprise hosts", + host: "enterprise.com", + query: query, + url: "https://enterprise.com/search?order=stars&q=keyword+stars%3A%3E%3D5+topic%3Atopic&sort=desc&type=repositories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.host == "" { + tt.host = "github.com" + } + searcher := NewSearcher(nil, tt.host) + assert.Equal(t, tt.url, searcher.URL(tt.query)) + }) + } +} diff --git a/pkg/text/convert.go b/pkg/text/convert.go new file mode 100644 index 000000000..c5d2f401d --- /dev/null +++ b/pkg/text/convert.go @@ -0,0 +1,29 @@ +package text + +import "unicode" + +// Copied from: https://github.com/asaskevich/govalidator +func CamelToKebab(str string) string { + var output []rune + var segment []rune + for _, r := range str { + if !unicode.IsLower(r) && string(r) != "-" && !unicode.IsNumber(r) { + output = addSegment(output, segment) + segment = nil + } + segment = append(segment, unicode.ToLower(r)) + } + output = addSegment(output, segment) + return string(output) +} + +func addSegment(inrune, segment []rune) []rune { + if len(segment) == 0 { + return inrune + } + if len(inrune) != 0 { + inrune = append(inrune, '-') + } + inrune = append(inrune, segment...) + return inrune +} diff --git a/pkg/text/convert_test.go b/pkg/text/convert_test.go new file mode 100644 index 000000000..5321fbf10 --- /dev/null +++ b/pkg/text/convert_test.go @@ -0,0 +1,61 @@ +package text + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCamelToKebab(t *testing.T) { + tests := []struct { + name string + in string + out string + }{ + { + name: "single lowercase word", + in: "test", + out: "test", + }, + { + name: "multiple mixed words", + in: "testTestTest", + out: "test-test-test", + }, + { + name: "multiple uppercase words", + in: "TestTest", + out: "test-test", + }, + { + name: "multiple lowercase words", + in: "testtest", + out: "testtest", + }, + { + name: "multiple mixed words with number", + in: "test2Test", + out: "test2-test", + }, + { + name: "multiple lowercase words with number", + in: "test2test", + out: "test2test", + }, + { + name: "multiple lowercase words with dash", + in: "test-test", + out: "test-test", + }, + { + name: "multiple uppercase words with dash", + in: "Test-Test", + out: "test--test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.out, CamelToKebab(tt.in)) + }) + } +} From 4d5ce7aa564eb9780939ecec6a69fc1d3665d5bd Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Wed, 9 Mar 2022 10:59:29 -0500 Subject: [PATCH 23/75] Adds internal codespace developer flags (#5287) --- .devcontainer/devcontainer.json | 5 +++++ internal/codespaces/api/api.go | 7 +++++++ pkg/cmd/codespace/create.go | 24 +++++++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..eac1bad83 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "extensions": [ + "golang.go" + ] +} \ No newline at end of file diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index afed04404..ec3a1ff66 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -579,6 +579,8 @@ type CreateCodespaceParams struct { Branch string Machine string Location string + VSCSTarget string + VSCSTargetURL string PermissionsOptOut bool } @@ -625,6 +627,8 @@ type startCreateRequest struct { Ref string `json:"ref"` Location string `json:"location"` Machine string `json:"machine"` + VSCSTarget string `json:"vscs_target,omitempty"` + VSCSTargetURL string `json:"vscs_target_url,omitempty"` PermissionsOptOut bool `json:"devcontainer_permissions_opt_out"` } @@ -654,8 +658,11 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (* Ref: params.Branch, Location: params.Location, Machine: params.Machine, + VSCSTarget: params.VSCSTarget, + VSCSTargetURL: params.VSCSTargetURL, PermissionsOptOut: params.PermissionsOptOut, }) + if err != nil { return nil, fmt.Errorf("error marshaling request: %w", err) } diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index b145a6a7d..26156a886 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "time" "github.com/AlecAivazis/survey/v2" @@ -47,7 +48,13 @@ func newCreateCmd(app *App) *cobra.Command { // Create creates a new Codespace func (a *App) Create(ctx context.Context, opts createOptions) error { - locationCh := getLocation(ctx, a.apiClient) + + // Overrides for Codespace developers to target test environments + vscsLocation := os.Getenv("VSCS_LOCATION") + vscsTarget := os.Getenv("VSCS_TARGET") + vscsTargetUrl := os.Getenv("VSCS_TARGET_URL") + + locationCh := getLocation(ctx, vscsLocation, a.apiClient) userInputs := struct { Repository string @@ -117,6 +124,8 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { Branch: branch, Machine: machine, Location: locationResult.Location, + VSCSTarget: vscsTarget, + VSCSTargetURL: vscsTargetUrl, IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), PermissionsOptOut: opts.permissionsOptOut, } @@ -282,9 +291,18 @@ type locationResult struct { Err error } -// getLocation fetches the closest Codespace datacenter region/location to the user. -func getLocation(ctx context.Context, apiClient apiClient) <-chan locationResult { +// getLocation fetches the closest Codespace datacenter +// region/location to the user, unless the 'vscsLocationOverride' override is set +func getLocation(ctx context.Context, vscsLocationOverride string, apiClient apiClient) <-chan locationResult { ch := make(chan locationResult, 1) + + // Developer override is set, return the override + if vscsLocationOverride != "" { + ch <- locationResult{vscsLocationOverride, nil} + return ch + } + + // Dynamically fetch the region location go func() { location, err := apiClient.GetCodespaceRegionLocation(ctx) ch <- locationResult{location, err} From ee1328de49876583df8ee04ff19c1ce5d3844e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 10 Mar 2022 17:28:39 +0100 Subject: [PATCH 24/75] Bump Cobra to eliminate Viper --- go.mod | 5 +- go.sum | 394 +++------------------------------------------------------ 2 files changed, 19 insertions(+), 380 deletions(-) diff --git a/go.mod b/go.mod index 59cd5006d..cd63fd72b 100644 --- a/go.mod +++ b/go.mod @@ -33,13 +33,14 @@ require ( github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect github.com/sourcegraph/jsonrpc2 v0.1.0 - github.com/spf13/cobra v1.3.0 + github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 - golang.org/x/term v0.0.0-20210503060354-a79de5458b56 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index b91f9192a..f51a48221 100644 --- a/go.sum +++ b/go.sum @@ -13,20 +13,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -35,7 +21,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -50,44 +35,22 @@ github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/glamour v0.4.0 h1:scR+smyB7WdmrlIaff6IVlm48P48JaNM7JypM/VGl4k= github.com/charmbracelet/glamour v0.4.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY= github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= @@ -101,17 +64,6 @@ github.com/cli/shurcooL-graphql v0.0.1 h1:/9J3t9O6p1B8zdBBtQighq5g7DQRItBwuwGh3S github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= @@ -124,38 +76,18 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -163,8 +95,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -177,13 +107,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -193,18 +118,11 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -212,69 +130,29 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/itchyny/gojq v0.12.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ= @@ -283,23 +161,13 @@ github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921i github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -307,20 +175,10 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -328,123 +186,59 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y= github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk= github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk= github.com/sourcegraph/jsonrpc2 v0.1.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= -github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -467,8 +261,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -477,25 +269,17 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -510,39 +294,19 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -551,35 +315,24 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -591,47 +344,24 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -651,7 +381,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -676,22 +405,9 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -713,30 +429,13 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -760,46 +459,12 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -812,22 +477,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -837,24 +486,13 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= From cb7315c85d3c0e010ba117ca7e692ed6f18f16c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 10 Mar 2022 18:29:42 +0100 Subject: [PATCH 25/75] Re-initialize modules cache in CI --- .github/workflows/go.yml | 9 ++++----- .github/workflows/lint.yml | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f3f5502ea..0d7e68d95 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -17,14 +17,13 @@ jobs: - name: Check out code uses: actions/checkout@v3 - - name: Cache Go modules + - name: Restore Go modules cache uses: actions/cache@v2 with: - path: ~/go - key: ${{ runner.os }}-build-${{ hashFiles('go.mod') }} + path: ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} restore-keys: | - ${{ runner.os }}-build- - ${{ runner.os }}- + go-${{ runner.os }}- - name: Download dependencies run: go mod download diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90e2c67e0..ea7d4ea34 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,6 +24,14 @@ jobs: - name: Check out code uses: actions/checkout@v3 + - name: Restore Go modules cache + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} + restore-keys: | + go-${{ runner.os }}- + - name: Verify dependencies run: | go mod verify From be5923770b3436f26df35c7c228353d5728a93a5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 16:42:31 -0600 Subject: [PATCH 26/75] Add vscs-target to `gh cs list` --- pkg/cmd/codespace/list.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 661d1da1c..6f6c902a1 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -43,6 +43,13 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er return fmt.Errorf("error getting codespaces: %w", err) } + hasNonDefaultVSCSTarget := false + for _, apiCodespace := range codespaces { + if apiCodespace.VSCSTarget != "prod" { + hasNonDefaultVSCSTarget = true + } + } + if err := a.io.StartPager(); err != nil { a.errLogger.Printf("error starting pager: %v", err) } @@ -59,6 +66,11 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField("BRANCH", nil, nil) tp.AddField("STATE", nil, nil) tp.AddField("CREATED AT", nil, nil) + + if hasNonDefaultVSCSTarget { + tp.AddField("VSCS TARGET", nil, nil) + } + tp.EndRow() } @@ -88,6 +100,11 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er } else { tp.AddField(c.CreatedAt, nil, nil) } + + if hasNonDefaultVSCSTarget { + tp.AddField(c.VSCSTarget, nil, nil) + } + tp.EndRow() } From d85feafa85b90c1783c4fe1411a2d992e9826ea0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 17:05:13 -0600 Subject: [PATCH 27/75] Rename to hasNonProdVSCSTarget --- pkg/cmd/codespace/list.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 6f6c902a1..2ee890670 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -43,10 +43,10 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er return fmt.Errorf("error getting codespaces: %w", err) } - hasNonDefaultVSCSTarget := false + hasNonProdVSCSTarget := false for _, apiCodespace := range codespaces { if apiCodespace.VSCSTarget != "prod" { - hasNonDefaultVSCSTarget = true + hasNonProdVSCSTarget = true } } @@ -67,7 +67,7 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField("STATE", nil, nil) tp.AddField("CREATED AT", nil, nil) - if hasNonDefaultVSCSTarget { + if hasNonProdVSCSTarget { tp.AddField("VSCS TARGET", nil, nil) } @@ -101,7 +101,7 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField(c.CreatedAt, nil, nil) } - if hasNonDefaultVSCSTarget { + if hasNonProdVSCSTarget { tp.AddField(c.VSCSTarget, nil, nil) } From a3b69e8d879ef4e961ac2f96490dbc2f9aa9f510 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 17:10:36 -0600 Subject: [PATCH 28/75] Add missed file --- internal/codespaces/api/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index ec3a1ff66..2e0a2153f 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -171,6 +171,7 @@ type Codespace struct { GitStatus CodespaceGitStatus `json:"git_status"` Connection CodespaceConnection `json:"connection"` Machine CodespaceMachine `json:"machine"` + VSCSTarget string `json:"vscs_target,omitempty"` } type CodespaceGitStatus struct { From 5e62a417d8c89d9d0cdc858298f1942661c58959 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 18:50:12 -0600 Subject: [PATCH 29/75] Add emojis to name --- internal/codespaces/api/api.go | 2 +- pkg/cmd/codespace/list.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 2e0a2153f..2a859f3ac 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -171,7 +171,7 @@ type Codespace struct { GitStatus CodespaceGitStatus `json:"git_status"` Connection CodespaceConnection `json:"connection"` Machine CodespaceMachine `json:"machine"` - VSCSTarget string `json:"vscs_target,omitempty"` + VSCSTarget string `json:"vscs_target"` } type CodespaceGitStatus struct { diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 2ee890670..987ed00af 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -86,7 +86,9 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er stateColor = cs.Green } - tp.AddField(c.Name, nil, cs.Yellow) + formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget) + + tp.AddField(formattedName, nil, cs.Yellow) tp.AddField(c.Repository.FullName, nil, nil) tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan) tp.AddField(c.State, nil, stateColor) @@ -110,3 +112,15 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er return tp.Render() } + +func formatNameForVSCSTarget(name, vscsTarget string) string { + if vscsTarget == "dev" || vscsTarget == "local" { + return fmt.Sprintf("%s 🚧", name) + } + + if vscsTarget == "ppe" { + return fmt.Sprintf("%s ✨", name) + } + + return name +} From 21a1059f7cf75c6b0e96ce323ef25c6675663029 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 19:01:55 -0600 Subject: [PATCH 30/75] Add `--profile` option to `gh cs cp` --- pkg/cmd/codespace/ssh.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 726f2152f..6ce783f2f 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -379,6 +379,7 @@ func newCpCmd(app *App) *cobra.Command { cpCmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "Recursively copy directories") cpCmd.Flags().BoolVarP(&opts.expand, "expand", "e", false, "Expand remote file names on remote shell") cpCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace") + cpCmd.Flags().StringVarP(&opts.profile, "profile", "p", "", "Name of the SSH profile to use") return cpCmd } From 94128d683c621a2d73a59bfddad371ff7d2bf3d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 19:59:24 -0600 Subject: [PATCH 31/75] Add consts for targets and treat empty as prod --- internal/codespaces/api/api.go | 7 +++++++ pkg/cmd/codespace/list.go | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 2a859f3ac..790ad0ee4 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -52,6 +52,13 @@ const ( vscsAPI = "https://online.visualstudio.com" ) +const ( + VSCSTargetLocal = "local" + VSCSTargetDevelopment = "development" + VSCSTargetPPE = "ppe" + VSCSTargetProduction = "production" +) + // API is the interface to the codespace service. type API struct { client httpClient diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 987ed00af..17c7b6414 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -45,8 +45,9 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er hasNonProdVSCSTarget := false for _, apiCodespace := range codespaces { - if apiCodespace.VSCSTarget != "prod" { + if apiCodespace.VSCSTarget != "" && apiCodespace.VSCSTarget != api.VSCSTargetProduction { hasNonProdVSCSTarget = true + break } } @@ -114,11 +115,11 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er } func formatNameForVSCSTarget(name, vscsTarget string) string { - if vscsTarget == "dev" || vscsTarget == "local" { + if vscsTarget == api.VSCSTargetDevelopment || vscsTarget == api.VSCSTargetLocal { return fmt.Sprintf("%s 🚧", name) } - if vscsTarget == "ppe" { + if vscsTarget == api.VSCSTargetPPE { return fmt.Sprintf("%s ✨", name) } From 9278f51aa89795cd56f0c4e71f0ffe74c6dc5fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 11 Mar 2022 14:58:15 +0100 Subject: [PATCH 32/75] Bump golangci-lint version --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea7d4ea34..9d5522ad7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,7 +37,7 @@ jobs: go mod verify go mod download - LINT_VERSION=1.39.0 + LINT_VERSION=1.44.2 curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \ tar xz --strip-components 1 --wildcards \*/golangci-lint mkdir -p bin && mv golangci-lint bin/ From de88d9e186bc690a44b620e8fb2c4ed80319680f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 11 Mar 2022 11:13:38 -0600 Subject: [PATCH 33/75] Only export vscsTarget for non-prod --- internal/codespaces/api/api.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 790ad0ee4..d8807ec74 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -226,6 +226,7 @@ var CodespaceFields = []string{ "createdAt", "lastUsedAt", "machineName", + "vscsTarget", } func (c *Codespace) ExportData(fields []string) map[string]interface{} { @@ -246,6 +247,10 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} { "hasUnpushedChanges": c.GitStatus.HasUnpushedChanges, "hasUncommitedChanges": c.GitStatus.HasUncommitedChanges, } + case "vscsTarget": + if c.VSCSTarget != "" && c.VSCSTarget != VSCSTargetProduction { + data[f] = c.VSCSTarget + } default: sf := v.FieldByNameFunc(func(s string) bool { return strings.EqualFold(f, s) From a03e9d3c957409a679c77dd244dbe871fc4a1b35 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Fri, 11 Mar 2022 22:53:01 +0000 Subject: [PATCH 34/75] review suggestions --- pkg/liveshare/ports.go | 6 +----- pkg/liveshare/rpc.go | 37 +++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index a1a9ffa39..7c9b0d87c 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -124,9 +124,5 @@ func (s *Session) GetSharedServers(ctx context.Context) ([]*Port, error) { // UpdateSharedServerPrivacy controls port permissions and visibility scopes for who can access its URLs // in the browser. func (s *Session) UpdateSharedServerPrivacy(ctx context.Context, port int, visibility string) error { - if err := s.rpc.do(ctx, "serverSharing.updateSharedServerPrivacy", []interface{}{port, visibility}, nil); err != nil { - return err - } - - return nil + return s.rpc.do(ctx, "serverSharing.updateSharedServerPrivacy", []interface{}{port, visibility}, nil) } diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 5972656aa..4d656a326 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -44,38 +44,39 @@ func (r *rpcClient) do(ctx context.Context, method string, args, result interfac type handlerFn func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) +type handlerSt struct { + fn handlerFn +} + type requestHandler struct { - handlersMu sync.RWMutex - handlers map[string][]handlerFn + handlersMu sync.Mutex + handlers map[string][]*handlerSt } func newRequestHandler() *requestHandler { - return &requestHandler{handlers: make(map[string][]handlerFn)} + return &requestHandler{handlers: make(map[string][]*handlerSt)} } -func (r *requestHandler) register(requestType string, handler handlerFn) func() { +func (r *requestHandler) register(requestType string, fn handlerFn) func() { r.handlersMu.Lock() defer r.handlersMu.Unlock() - if _, ok := r.handlers[requestType]; !ok { - r.handlers[requestType] = []handlerFn{} - } - - r.handlers[requestType] = append(r.handlers[requestType], handler) + h := &handlerSt{fn: fn} + r.handlers[requestType] = append(r.handlers[requestType], h) return func() { - r.deregister(requestType, handler) + r.deregister(requestType, h) } } -func (r *requestHandler) deregister(requestType string, handler handlerFn) { +func (r *requestHandler) deregister(requestType string, handler *handlerSt) { r.handlersMu.Lock() defer r.handlersMu.Unlock() if handlers, ok := r.handlers[requestType]; ok { - newHandlers := []handlerFn{} + newHandlers := []*handlerSt{} for _, h := range handlers { - if &h != &handler { + if h != handler { newHandlers = append(newHandlers, h) } } @@ -87,15 +88,15 @@ func (r *requestHandler) deregister(requestType string, handler handlerFn) { } } -func (r *requestHandler) handlerFn(requestType string) []handlerFn { - r.handlersMu.RLock() - defer r.handlersMu.RUnlock() +func (r *requestHandler) getHandlers(requestType string) []*handlerSt { + r.handlersMu.Lock() + defer r.handlersMu.Unlock() return r.handlers[requestType] } func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - for _, handler := range r.handlerFn(req.Method) { - go handler(conn, req) + for _, handler := range r.getHandlers(req.Method) { + go handler.fn(conn, req) } } From ca7e2d386d293585b76f4f8477db287b05df662f Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Fri, 11 Mar 2022 23:34:58 +0000 Subject: [PATCH 35/75] review suggestion: non-blocking send --- pkg/liveshare/ports.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index 7c9b0d87c..6e7097074 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -77,18 +77,26 @@ type PortNotification struct { // or an error if the notification is not received before the context is cancelled or it fails // to parse the notification. func (s *Session) WaitForPortNotification(ctx context.Context, port int, notifType PortChangeKind) (*PortNotification, error) { - notificationUpdate := make(chan PortNotification, 1) - errc := make(chan error, 1) + notificationCh := make(chan *PortNotification, 1) + errCh := make(chan error, 1) h := func(success bool) func(*jsonrpc2.Conn, *jsonrpc2.Request) { return func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - var notification PortNotification + notification := new(PortNotification) if err := json.Unmarshal(*req.Params, ¬ification); err != nil { - errc <- fmt.Errorf("error unmarshaling notification: %w", err) + select { + case errCh <- fmt.Errorf("error unmarshaling notification: %w", err): + default: + } return } notification.Success = success - notificationUpdate <- notification + if notification.Port == port && notification.ChangeKind == notifType { + select { + case notificationCh <- notification: + default: + } + } } } deregisterSuccess := s.registerRequestHandler("serverSharing.sharingSucceeded", h(true)) @@ -100,12 +108,10 @@ func (s *Session) WaitForPortNotification(ctx context.Context, port int, notifTy select { case <-ctx.Done(): return nil, ctx.Err() - case err := <-errc: + case err := <-errCh: return nil, err - case notification := <-notificationUpdate: - if notification.Port == port && notification.ChangeKind == notifType { - return ¬ification, nil - } + case notification := <-notificationCh: + return notification, nil } } } From bb9bf298353e62cc83a89d472ae4ca8b38c2ff28 Mon Sep 17 00:00:00 2001 From: Boston Cartwright Date: Mon, 14 Mar 2022 06:02:57 -0600 Subject: [PATCH 36/75] pr merge switch to base branch if available (#5251) * after merge, switch to base branch if available * Add ability to checkout new branch * Style cleanup Co-authored-by: Sam Coe --- git/git.go | 9 +++ pkg/cmd/pr/merge/merge.go | 45 +++++++------- pkg/cmd/pr/merge/merge_test.go | 103 +++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 24 deletions(-) diff --git a/git/git.go b/git/git.go index 7a3a81437..31fea6233 100644 --- a/git/git.go +++ b/git/git.go @@ -307,6 +307,15 @@ func CheckoutBranch(branch string) error { return run.PrepareCmd(configCmd).Run() } +func CheckoutNewBranch(remoteName, branch string) error { + track := fmt.Sprintf("%s/%s", remoteName, branch) + configCmd, err := GitCommand("checkout", "-b", branch, "--track", track) + if err != nil { + return err + } + return run.PrepareCmd(configCmd).Run() +} + // pull changes from remote branch without version history func Pull(remote, branch string) error { pullCmd, err := GitCommand("pull", "--ff-only", remote, branch) diff --git a/pkg/cmd/pr/merge/merge.go b/pkg/cmd/pr/merge/merge.go index e13d08b7a..f6d5fb37b 100644 --- a/pkg/cmd/pr/merge/merge.go +++ b/pkg/cmd/pr/merge/merge.go @@ -11,7 +11,6 @@ import ( "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/config" - "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -322,18 +321,35 @@ func mergeRun(opts *MergeOptions) error { var branchToSwitchTo string if currentBranch == pr.HeadRefName { - branchToSwitchTo, err = api.RepoDefaultBranch(apiClient, baseRepo) + branchToSwitchTo = pr.BaseRefName + if branchToSwitchTo == "" { + branchToSwitchTo, err = api.RepoDefaultBranch(apiClient, baseRepo) + if err != nil { + return err + } + } + + remotes, err := opts.Remotes() if err != nil { return err } - err = git.CheckoutBranch(branchToSwitchTo) + baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()) if err != nil { return err } - err := pullLatestChanges(opts, baseRepo, branchToSwitchTo) - if err != nil { + if git.HasLocalBranch(branchToSwitchTo) { + if err := git.CheckoutBranch(branchToSwitchTo); err != nil { + return err + } + } else { + if err := git.CheckoutNewBranch(baseRemote.Name, branchToSwitchTo); err != nil { + return err + } + } + + if err := git.Pull(baseRemote.Name, branchToSwitchTo); err != nil { fmt.Fprintf(opts.IO.ErrOut, "%s warning: not possible to fast-forward to: %q\n", cs.WarningIcon(), branchToSwitchTo) } } @@ -364,25 +380,6 @@ func mergeRun(opts *MergeOptions) error { return nil } -func pullLatestChanges(opts *MergeOptions, repo ghrepo.Interface, branch string) error { - remotes, err := opts.Remotes() - if err != nil { - return err - } - - baseRemote, err := remotes.FindByRepo(repo.RepoOwner(), repo.RepoName()) - if err != nil { - return err - } - - err = git.Pull(baseRemote.Name, branch) - if err != nil { - return err - } - - return nil -} - func mergeMethodSurvey(baseRepo *api.Repository) (PullRequestMergeMethod, error) { type mergeOption struct { title string diff --git a/pkg/cmd/pr/merge/merge_test.go b/pkg/cmd/pr/merge/merge_test.go index d692b0a08..c7f30f7c4 100644 --- a/pkg/cmd/pr/merge/merge_test.go +++ b/pkg/cmd/pr/merge/merge_test.go @@ -480,6 +480,7 @@ func TestPrMerge_deleteBranch(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git rev-parse --verify refs/heads/master`, 0, "") cs.Register(`git checkout master`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") @@ -497,6 +498,106 @@ func TestPrMerge_deleteBranch(t *testing.T) { `), output.Stderr()) } +func TestPrMerge_deleteBranch_nonDefault(t *testing.T) { + http := initFakeHTTP() + defer http.Verify(t) + + shared.RunCommandFinder( + "", + &api.PullRequest{ + ID: "PR_10", + Number: 10, + State: "OPEN", + Title: "Blueberries are a good fruit", + HeadRefName: "blueberries", + MergeStateStatus: "CLEAN", + BaseRefName: "fruit", + }, + baseRepo("OWNER", "REPO", "master"), + ) + + http.Register( + httpmock.GraphQL(`mutation PullRequestMerge\b`), + httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { + assert.Equal(t, "PR_10", input["pullRequestId"].(string)) + assert.Equal(t, "MERGE", input["mergeMethod"].(string)) + assert.NotContains(t, input, "commitHeadline") + })) + http.Register( + httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), + httpmock.StringResponse(`{}`)) + + cs, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + cs.Register(`git rev-parse --verify refs/heads/fruit`, 0, "") + cs.Register(`git checkout fruit`, 0, "") + cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") + cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git pull --ff-only`, 0, "") + + output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`) + if err != nil { + t.Fatalf("Got unexpected error running `pr merge` %s", err) + } + + assert.Equal(t, "", output.String()) + assert.Equal(t, heredoc.Doc(` + ✓ Merged pull request #10 (Blueberries are a good fruit) + ✓ Deleted branch blueberries and switched to branch fruit + `), output.Stderr()) +} + +func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) { + http := initFakeHTTP() + defer http.Verify(t) + + shared.RunCommandFinder( + "", + &api.PullRequest{ + ID: "PR_10", + Number: 10, + State: "OPEN", + Title: "Blueberries are a good fruit", + HeadRefName: "blueberries", + MergeStateStatus: "CLEAN", + BaseRefName: "fruit", + }, + baseRepo("OWNER", "REPO", "master"), + ) + + http.Register( + httpmock.GraphQL(`mutation PullRequestMerge\b`), + httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) { + assert.Equal(t, "PR_10", input["pullRequestId"].(string)) + assert.Equal(t, "MERGE", input["mergeMethod"].(string)) + assert.NotContains(t, input, "commitHeadline") + })) + http.Register( + httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"), + httpmock.StringResponse(`{}`)) + + cs, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + cs.Register(`git rev-parse --verify refs/heads/fruit`, 1, "") + cs.Register(`git checkout -b fruit --track origin/fruit`, 0, "") + cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") + cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git pull --ff-only`, 0, "") + + output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`) + if err != nil { + t.Fatalf("Got unexpected error running `pr merge` %s", err) + } + + assert.Equal(t, "", output.String()) + assert.Equal(t, heredoc.Doc(` + ✓ Merged pull request #10 (Blueberries are a good fruit) + ✓ Deleted branch blueberries and switched to branch fruit + `), output.Stderr()) +} + func TestPrMerge_deleteNonCurrentBranch(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) @@ -764,6 +865,7 @@ func TestPrMerge_alreadyMerged(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git rev-parse --verify refs/heads/master`, 0, "") cs.Register(`git checkout master`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") @@ -906,6 +1008,7 @@ func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git rev-parse --verify refs/heads/master`, 0, "") cs.Register(`git checkout master`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") From 07e0e52edd52bb46a5874767ca3b4fac50caa7c2 Mon Sep 17 00:00:00 2001 From: neilnaveen <42328488+neilnaveen@users.noreply.github.com> Date: Mon, 14 Mar 2022 08:18:21 -0500 Subject: [PATCH 37/75] Fixed permission for workflow (#5279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions Co-authored-by: Mislav Marohnić --- .github/workflows/codeql.yml | 5 +++++ .github/workflows/go.yml | 4 ++++ .github/workflows/issueauto.yml | 13 +++++++++---- .github/workflows/lint.yml | 3 +++ .github/workflows/prauto.yml | 6 ++++++ .github/workflows/releases.yml | 4 ++++ 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 720f1210e..034253caf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,6 +10,11 @@ on: schedule: - cron: "0 0 * * 0" +permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/analyze to upload SARIF results + jobs: CodeQL-Build: runs-on: ubuntu-latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0d7e68d95..4fbf3d3b9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,5 +1,9 @@ name: Tests on: [push, pull_request] + +permissions: + contents: read + jobs: build: strategy: diff --git a/.github/workflows/issueauto.yml b/.github/workflows/issueauto.yml index a366d6ed8..40c4b36e7 100644 --- a/.github/workflows/issueauto.yml +++ b/.github/workflows/issueauto.yml @@ -2,16 +2,21 @@ name: Issue Automation on: issues: types: [opened] + +permissions: + contents: none + issues: write + jobs: issue-auto: runs-on: ubuntu-latest steps: - name: label incoming issue env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} - ISSUENUM: ${{ github.event.issue.number }} - ISSUEAUTHOR: ${{ github.event.issue.user.login }} + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + ISSUENUM: ${{ github.event.issue.number }} + ISSUEAUTHOR: ${{ github.event.issue.user.login }} run: | if ! gh api orgs/cli/public_members/$ISSUEAUTHOR --silent 2>/dev/null then diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea7d4ea34..e811ef6ef 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,6 +11,9 @@ on: - go.mod - go.sum +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml index 047fb52ea..2596fa76c 100644 --- a/.github/workflows/prauto.yml +++ b/.github/workflows/prauto.yml @@ -2,6 +2,12 @@ name: PR Automation on: pull_request_target: types: [ready_for_review, opened, reopened] + +permissions: + contents: none + issues: write + pull-requests: write + jobs: pr-auto: runs-on: ubuntu-latest diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 3e7f6cf2f..5e570350a 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -5,6 +5,10 @@ on: tags: - "v*" +permissions: + contents: write # publishing releases + repository-projects: write # move cards between columns + jobs: goreleaser: runs-on: ubuntu-latest From ed376f3691cd9c5d198b2279242f262e47624549 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 14 Mar 2022 10:29:31 -0400 Subject: [PATCH 38/75] Pass conn to handlers instead of obj stream --- pkg/cmd/codespace/ports_test.go | 15 ++++++-------- pkg/liveshare/client_test.go | 2 +- pkg/liveshare/port_forwarder_test.go | 26 ++++++++++++++--------- pkg/liveshare/session_test.go | 31 ++++++++++++++-------------- pkg/liveshare/test/server.go | 15 ++------------ 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index 1cbf50103..4b1db27b4 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -154,7 +154,7 @@ type joinWorkspaceResult struct { func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, eventResponses []string, portsData []liveshare.PortNotification) error { t.Helper() - joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { + joinWorkspace := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } const sessionToken = "session-token" @@ -162,14 +162,14 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev ctx, cancel := context.WithCancel(context.Background()) defer cancel() - ch := make(chan float64, 1) - updateSharedVisibility := func(rpcReq *jsonrpc2.Request) (interface{}, error) { + ch := make(chan *jsonrpc2.Conn, 1) + updateSharedVisibility := func(conn *jsonrpc2.Conn, rpcReq *jsonrpc2.Request) (interface{}, error) { var req []interface{} if err := json.Unmarshal(*rpcReq.Params, &req); err != nil { return nil, fmt.Errorf("unmarshal req: %w", err) } - ch <- req[0].(float64) + ch <- conn return nil, nil } testServer, err := livesharetest.NewServer( @@ -193,12 +193,9 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev select { case <-ctx.Done(): return - case <-ch: + case conn := <-ch: pd := portsData[i] - _ = testServer.WriteToObjectStream(rpcMessage{ - Method: eventResponses[i], - Params: pd.PortUpdate, - }) + _, _ = conn.DispatchCall(context.Background(), eventResponses[i], pd.PortUpdate, nil) } } }() diff --git a/pkg/liveshare/client_test.go b/pkg/liveshare/client_test.go index c9c03aa3d..4b2908858 100644 --- a/pkg/liveshare/client_test.go +++ b/pkg/liveshare/client_test.go @@ -22,7 +22,7 @@ func TestConnect(t *testing.T) { HostPublicKeys: []string{livesharetest.SSHPublicKey}, Logger: newMockLogger(), } - joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { + joinWorkspace := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { var joinWorkspaceReq joinWorkspaceArgs if err := json.Unmarshal(*req.Params, &joinWorkspaceReq); err != nil { return nil, fmt.Errorf("error unmarshaling req: %w", err) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index 464b17a34..6281a2f86 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -26,18 +26,26 @@ func TestNewPortForwarder(t *testing.T) { } } +type portUpdateNotification struct { + PortUpdate + conn *jsonrpc2.Conn +} + func TestPortForwarderStart(t *testing.T) { streamName, streamCondition := "stream-name", "stream-condition" port := 8000 - sendNotification := make(chan PortUpdate) - serverSharing := func(req *jsonrpc2.Request) (interface{}, error) { - sendNotification <- PortUpdate{ - Port: int(port), - ChangeKind: PortChangeKindStart, + sendNotification := make(chan portUpdateNotification) + serverSharing := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { + sendNotification <- portUpdateNotification{ + PortUpdate: PortUpdate{ + Port: int(port), + ChangeKind: PortChangeKindStart, + }, + conn: conn, } return Port{StreamName: streamName, StreamCondition: streamCondition}, nil } - getStream := func(req *jsonrpc2.Request) (interface{}, error) { + getStream := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { return "stream-id", nil } @@ -62,10 +70,8 @@ func TestPortForwarderStart(t *testing.T) { defer cancel() go func() { - _ = testServer.WriteToObjectStream(rpcPortTestMessage{ - Method: "serverSharing.sharingSucceeded", - Params: <-sendNotification, - }) + notif := <-sendNotification + _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif.PortUpdate) }() done := make(chan error) diff --git a/pkg/liveshare/session_test.go b/pkg/liveshare/session_test.go index 0067e3350..dc8e394b4 100644 --- a/pkg/liveshare/session_test.go +++ b/pkg/liveshare/session_test.go @@ -19,7 +19,7 @@ import ( const mockClientName = "liveshare-client" func makeMockSession(opts ...livesharetest.ServerOption) (*livesharetest.Server, *Session, error) { - joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { + joinWorkspace := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } const sessionToken = "session-token" @@ -56,8 +56,8 @@ type rpcPortTestMessage struct { func TestServerStartSharing(t *testing.T) { serverPort, serverProtocol := 2222, "sshd" - sendNotification := make(chan PortUpdate) - startSharing := func(req *jsonrpc2.Request) (interface{}, error) { + sendNotification := make(chan portUpdateNotification) + startSharing := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { var args []interface{} if err := json.Unmarshal(*req.Params, &args); err != nil { return nil, fmt.Errorf("error unmarshaling request: %w", err) @@ -82,9 +82,12 @@ func TestServerStartSharing(t *testing.T) { } else if browseURL != fmt.Sprintf("http://localhost:%d", serverPort) { return nil, errors.New("browseURL does not match expected") } - sendNotification <- PortUpdate{ - Port: int(port), - ChangeKind: PortChangeKindStart, + sendNotification <- portUpdateNotification{ + PortUpdate: PortUpdate{ + Port: int(port), + ChangeKind: PortChangeKindStart, + }, + conn: conn, } return Port{StreamName: "stream-name", StreamCondition: "stream-condition"}, nil } @@ -99,10 +102,8 @@ func TestServerStartSharing(t *testing.T) { ctx := context.Background() go func() { - _ = testServer.WriteToObjectStream(rpcPortTestMessage{ - Method: "serverSharing.sharingSucceeded", - Params: <-sendNotification, - }) + notif := <-sendNotification + _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif.PortUpdate) }() done := make(chan error) @@ -133,7 +134,7 @@ func TestServerGetSharedServers(t *testing.T) { StreamName: "stream-name", StreamCondition: "stream-condition", } - getSharedServers := func(req *jsonrpc2.Request) (interface{}, error) { + getSharedServers := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { return []*Port{&sharedServer}, nil } testServer, session, err := makeMockSession( @@ -176,7 +177,7 @@ func TestServerGetSharedServers(t *testing.T) { } func TestServerUpdateSharedServerPrivacy(t *testing.T) { - updateSharedVisibility := func(rpcReq *jsonrpc2.Request) (interface{}, error) { + updateSharedVisibility := func(conn *jsonrpc2.Conn, rpcReq *jsonrpc2.Request) (interface{}, error) { var req []interface{} if err := json.Unmarshal(*rpcReq.Params, &req); err != nil { return nil, fmt.Errorf("unmarshal req: %w", err) @@ -223,7 +224,7 @@ func TestServerUpdateSharedServerPrivacy(t *testing.T) { } func TestInvalidHostKey(t *testing.T) { - joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) { + joinWorkspace := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { return joinWorkspaceResult{1}, nil } const sessionToken = "session-token" @@ -259,7 +260,7 @@ func TestKeepAliveNonBlocking(t *testing.T) { } func TestNotifyHostOfActivity(t *testing.T) { - notifyHostOfActivity := func(rpcReq *jsonrpc2.Request) (interface{}, error) { + notifyHostOfActivity := func(conn *jsonrpc2.Conn, rpcReq *jsonrpc2.Request) (interface{}, error) { var req []interface{} if err := json.Unmarshal(*rpcReq.Params, &req); err != nil { return nil, fmt.Errorf("unmarshal req: %w", err) @@ -318,7 +319,7 @@ func TestSessionHeartbeat(t *testing.T) { wg sync.WaitGroup ) wg.Add(1) - notifyHostOfActivity := func(rpcReq *jsonrpc2.Request) (interface{}, error) { + notifyHostOfActivity := func(conn *jsonrpc2.Conn, rpcReq *jsonrpc2.Request) (interface{}, error) { defer wg.Done() requestsMu.Lock() requests++ diff --git a/pkg/liveshare/test/server.go b/pkg/liveshare/test/server.go index 7f5340db4..793b714f0 100644 --- a/pkg/liveshare/test/server.go +++ b/pkg/liveshare/test/server.go @@ -43,8 +43,6 @@ type Server struct { httptestServer *httptest.Server errCh chan error nonSecure bool - - objectStream jsonrpc2.ObjectStream } // NewServer creates a new Server. ServerOptions can be passed to configure @@ -149,13 +147,6 @@ func (s *Server) Err() <-chan error { return s.errCh } -func (s *Server) WriteToObjectStream(obj interface{}) error { - if s.objectStream == nil { - return errors.New("object stream not set") - } - return s.objectStream.WriteObject(obj) -} - var upgrader = websocket.Upgrader{} func makeConnection(server *Server) http.HandlerFunc { @@ -322,12 +313,10 @@ func forwardStream(ctx context.Context, server *Server, streamName string, chann func handleChannel(server *Server, channel ssh.Channel) { stream := jsonrpc2.NewBufferedStream(channel, jsonrpc2.VSCodeObjectCodec{}) - server.objectStream = stream - jsonrpc2.NewConn(context.Background(), stream, newRPCHandler(server)) } -type RPCHandleFunc func(req *jsonrpc2.Request) (interface{}, error) +type RPCHandleFunc func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) type rpcHandler struct { server *Server @@ -346,7 +335,7 @@ func (r *rpcHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonr return } - result, err := handler(req) + result, err := handler(conn, req) if err != nil { sendError(r.server.errCh, fmt.Errorf("error handling: '%s': %w", req.Method, err)) return From bc80675b6f036f2ce685d292ab10ab6a3daa63b3 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 14 Mar 2022 10:34:52 -0400 Subject: [PATCH 39/75] Remove unused types --- pkg/cmd/codespace/ports_test.go | 5 ----- pkg/liveshare/session_test.go | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index 4b1db27b4..c15e519ee 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -182,11 +182,6 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev return fmt.Errorf("unable to create test server: %w", err) } - type rpcMessage struct { - Method string - Params liveshare.PortUpdate - } - go func() { var i int for ; ; i++ { diff --git a/pkg/liveshare/session_test.go b/pkg/liveshare/session_test.go index dc8e394b4..85d26eb09 100644 --- a/pkg/liveshare/session_test.go +++ b/pkg/liveshare/session_test.go @@ -49,11 +49,6 @@ func makeMockSession(opts ...livesharetest.ServerOption) (*livesharetest.Server, return testServer, session, nil } -type rpcPortTestMessage struct { - Method string - Params PortUpdate -} - func TestServerStartSharing(t *testing.T) { serverPort, serverProtocol := 2222, "sshd" sendNotification := make(chan portUpdateNotification) From 3645975da763411b0c0d9b7482be5bed774b6509 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 10:00:29 -0700 Subject: [PATCH 40/75] Prefer IsStdoutTTY when that's all we need --- pkg/cmd/run/rerun/rerun.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 21a3c31d1..e382fd134 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -124,7 +124,7 @@ func runRerun(opts *RerunOptions) error { if err != nil { return err } - if opts.IO.CanPrompt() { + if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s\n", cs.SuccessIcon(), cs.Cyanf("%d", selectedJob.ID), @@ -142,7 +142,7 @@ func runRerun(opts *RerunOptions) error { if err != nil { return err } - if opts.IO.CanPrompt() { + if opts.IO.IsStdoutTTY() { onlyFailedMsg := "" if opts.OnlyFailed { onlyFailedMsg = "(failed jobs) " From 24ec53365bdf505116485189ec432bcc19adfca0 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 10:01:19 -0700 Subject: [PATCH 41/75] Return error if both jobID and runID are specified --- pkg/cmd/run/rerun/rerun.go | 6 +----- pkg/cmd/run/rerun/rerun_test.go | 10 ++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index e382fd134..0303060b4 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -50,11 +50,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } if opts.RunID != "" && opts.JobID != "" { - opts.RunID = "" - if opts.IO.CanPrompt() { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon()) - } + return cmdutil.FlagErrorf("specify only one of or ") } if runF != nil { diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 094425290..b7bfc1d62 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -76,12 +76,10 @@ func TestNewCmdRerun(t *testing.T) { }, }, { - name: "with args job and runID ignores runID", - tty: true, - cli: "1234 --job 5678", - wants: RerunOptions{ - JobID: "5678", - }, + name: "with args jobID and runID fails", + tty: true, + cli: "1234 --job 5678", + wantsErr: true, }, { name: "with arg job with no ID fails", From 83a6cccf2ae36e4180aa0d6ab5a18b52311870db Mon Sep 17 00:00:00 2001 From: Gowtham Munukutla Date: Mon, 14 Mar 2022 23:19:03 +0530 Subject: [PATCH 42/75] Add interactive repository edit functionality (#4895) --- api/queries_repo.go | 3 + pkg/cmd/repo/edit/edit.go | 339 ++++++++++++++++++++++++++++++--- pkg/cmd/repo/edit/edit_test.go | 192 ++++++++++++++++++- 3 files changed, 499 insertions(+), 35 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index b7ea14915..a287b6557 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -46,6 +46,7 @@ type Repository struct { MergeCommitAllowed bool SquashMergeAllowed bool RebaseMergeAllowed bool + AutoMergeAllowed bool ForkCount int StargazerCount int @@ -68,6 +69,7 @@ type Repository struct { IsArchived bool IsEmpty bool IsFork bool + ForkingAllowed bool IsInOrganization bool IsMirror bool IsPrivate bool @@ -81,6 +83,7 @@ type Repository struct { ViewerPermission string ViewerPossibleCommitEmails []string ViewerSubscription string + Visibility string RepositoryTopics struct { Nodes []struct { diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 86ccdaab1..1793caad2 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -8,44 +8,73 @@ import ( "io" "io/ioutil" "net/http" + "strings" + "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/cli/cli/v2/pkg/set" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) +const ( + allowMergeCommits = "Allow Merge Commits" + allowSquashMerge = "Allow Squash Merging" + allowRebaseMerge = "Allow Rebase Merging" + + optionAllowForking = "Allow Forking" + optionDefaultBranchName = "Default Branch Name" + optionDescription = "Description" + optionHomePageURL = "Home Page URL" + optionIssues = "Issues" + optionMergeOptions = "Merge Options" + optionProjects = "Projects" + optionTemplateRepo = "Template Repository" + optionTopics = "Topics" + optionVisibility = "Visibility" + optionWikis = "Wikis" +) + type EditOptions struct { - HTTPClient *http.Client - Repository ghrepo.Interface - Edits EditRepositoryInput - AddTopics []string - RemoveTopics []string + HTTPClient *http.Client + Repository ghrepo.Interface + IO *iostreams.IOStreams + Edits EditRepositoryInput + AddTopics []string + RemoveTopics []string + InteractiveMode bool + // Cache of current repo topics to avoid retrieving them + // in multiple flows. + topicsCache []string } type EditRepositoryInput struct { - Description *string `json:"description,omitempty"` - Homepage *string `json:"homepage,omitempty"` - Visibility *string `json:"visibility,omitempty"` - EnableIssues *bool `json:"has_issues,omitempty"` - EnableProjects *bool `json:"has_projects,omitempty"` - EnableWiki *bool `json:"has_wiki,omitempty"` - IsTemplate *bool `json:"is_template,omitempty"` - DefaultBranch *string `json:"default_branch,omitempty"` - EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` - EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` - EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` - EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` - DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` AllowForking *bool `json:"allow_forking,omitempty"` + DefaultBranch *string `json:"default_branch,omitempty"` + DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"` + Description *string `json:"description,omitempty"` + EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"` + EnableIssues *bool `json:"has_issues,omitempty"` + EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"` + EnableProjects *bool `json:"has_projects,omitempty"` + EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"` + EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"` + EnableWiki *bool `json:"has_wiki,omitempty"` + Homepage *string `json:"homepage,omitempty"` + IsTemplate *bool `json:"is_template,omitempty"` + Visibility *string `json:"visibility,omitempty"` } func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command { - opts := &EditOptions{} + opts := &EditOptions{ + IO: f.IOStreams, + } cmd := &cobra.Command{ Use: "edit []", @@ -71,10 +100,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr gh repo edit --enable-projects=false `), RunE: func(cmd *cobra.Command, args []string) error { - if cmd.Flags().NFlag() == 0 { - return cmdutil.FlagErrorf("at least one flag is required") - } - if len(args) > 0 { var err error opts.Repository, err = ghrepo.FromFullName(args[0]) @@ -95,6 +120,14 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr return err } + if cmd.Flags().NFlag() == 0 { + opts.InteractiveMode = true + } + + if opts.InteractiveMode && !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("specify properties to edit when not running interactively") + } + if runF != nil { return runF(opts) } @@ -124,6 +157,38 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr func editRun(ctx context.Context, opts *EditOptions) error { repo := opts.Repository + + if opts.InteractiveMode { + apiClient := api.NewClientFromHTTP(opts.HTTPClient) + fieldsToRetrieve := []string{ + "autoMergeAllowed", + "defaultBranchRef", + "deleteBranchOnMerge", + "description", + "hasIssuesEnabled", + "hasProjectsEnabled", + "hasWikiEnabled", + "homepageUrl", + "isInOrganization", + "isTemplate", + "mergeCommitAllowed", + "rebaseMergeAllowed", + "repositoryTopics", + "squashMergeAllowed", + "visibility", + } + opts.IO.StartProgressIndicator() + fetchedRepo, err := api.FetchRepository(apiClient, opts.Repository, fieldsToRetrieve) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + err = interactiveRepoEdit(opts, fetchedRepo) + if err != nil { + return err + } + } + apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName()) body := &bytes.Buffer{} @@ -144,16 +209,19 @@ func editRun(ctx context.Context, opts *EditOptions) error { if len(opts.AddTopics) > 0 || len(opts.RemoveTopics) > 0 { g.Go(func() error { - existingTopics, err := getTopics(ctx, opts.HTTPClient, repo) - if err != nil { - return err + // opts.topicsCache gets populated in interactive mode + if !opts.InteractiveMode { + var err error + opts.topicsCache, err = getTopics(ctx, opts.HTTPClient, repo) + if err != nil { + return err + } } - oldTopics := set.NewStringSet() - oldTopics.AddValues(existingTopics) + oldTopics.AddValues(opts.topicsCache) newTopics := set.NewStringSet() - newTopics.AddValues(existingTopics) + newTopics.AddValues(opts.topicsCache) newTopics.AddValues(opts.AddTopics) newTopics.RemoveValues(opts.RemoveTopics) @@ -164,7 +232,209 @@ func editRun(ctx context.Context, opts *EditOptions) error { }) } - return g.Wait() + err := g.Wait() + if err != nil { + return err + } + + if opts.IO.IsStdoutTTY() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.Out, + "%s Edited repository %s\n", + cs.SuccessIcon(), + ghrepo.FullName(repo)) + } + + return nil +} + +func interactiveChoice(r *api.Repository) ([]string, error) { + options := []string{ + optionDefaultBranchName, + optionDescription, + optionHomePageURL, + optionIssues, + optionMergeOptions, + optionProjects, + optionTemplateRepo, + optionTopics, + optionVisibility, + optionWikis, + } + if r.IsInOrganization { + options = append(options, optionAllowForking) + } + var answers []string + err := prompt.SurveyAskOne(&survey.MultiSelect{ + Message: "What do you want to edit?", + Options: options, + }, &answers, survey.WithPageSize(11)) + return answers, err +} + +func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error { + for _, v := range r.RepositoryTopics.Nodes { + opts.topicsCache = append(opts.topicsCache, v.Topic.Name) + } + choices, err := interactiveChoice(r) + if err != nil { + return err + } + for _, c := range choices { + switch c { + case optionDescription: + opts.Edits.Description = &r.Description + err = prompt.SurveyAskOne(&survey.Input{ + Message: "Description of the repository", + Default: r.Description, + }, opts.Edits.Description) + if err != nil { + return err + } + case optionHomePageURL: + opts.Edits.Homepage = &r.HomepageURL + err = prompt.SurveyAskOne(&survey.Input{ + Message: "Repository home page URL", + Default: r.HomepageURL, + }, opts.Edits.Homepage) + if err != nil { + return err + } + case optionTopics: + var addTopics string + err = prompt.SurveyAskOne(&survey.Input{ + Message: "Add topics?(csv format)", + }, &addTopics) + if err != nil { + return err + } + if len(strings.TrimSpace(addTopics)) > 0 { + opts.AddTopics = strings.Split(addTopics, ",") + } + + if len(opts.topicsCache) > 0 { + err = prompt.SurveyAskOne(&survey.MultiSelect{ + Message: "Remove Topics", + Options: opts.topicsCache, + }, &opts.RemoveTopics) + if err != nil { + return err + } + } + case optionDefaultBranchName: + opts.Edits.DefaultBranch = &r.DefaultBranchRef.Name + err = prompt.SurveyAskOne(&survey.Input{ + Message: "Default branch name", + Default: r.DefaultBranchRef.Name, + }, opts.Edits.DefaultBranch) + if err != nil { + return err + } + case optionWikis: + opts.Edits.EnableWiki = &r.HasWikiEnabled + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Enable Wikis?", + Default: r.HasWikiEnabled, + }, opts.Edits.EnableWiki) + if err != nil { + return err + } + case optionIssues: + opts.Edits.EnableIssues = &r.HasIssuesEnabled + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Enable Issues?", + Default: r.HasIssuesEnabled, + }, opts.Edits.EnableIssues) + if err != nil { + return err + } + case optionProjects: + opts.Edits.EnableProjects = &r.HasProjectsEnabled + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Enable Projects?", + Default: r.HasProjectsEnabled, + }, opts.Edits.EnableProjects) + if err != nil { + return err + } + case optionVisibility: + opts.Edits.Visibility = &r.Visibility + err = prompt.SurveyAskOne(&survey.Select{ + Message: "Visibility", + Options: []string{"public", "private", "internal"}, + Default: strings.ToLower(r.Visibility), + }, opts.Edits.Visibility) + if err != nil { + return err + } + case optionMergeOptions: + var defaultMergeOptions []string + var selectedMergeOptions []string + if r.MergeCommitAllowed { + defaultMergeOptions = append(defaultMergeOptions, allowMergeCommits) + } + if r.SquashMergeAllowed { + defaultMergeOptions = append(defaultMergeOptions, allowSquashMerge) + } + if r.RebaseMergeAllowed { + defaultMergeOptions = append(defaultMergeOptions, allowRebaseMerge) + } + err = prompt.SurveyAskOne(&survey.MultiSelect{ + Message: "Allowed merge strategies", + Default: defaultMergeOptions, + Options: []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge}, + }, &selectedMergeOptions) + if err != nil { + return err + } + enableMergeCommit := isIncluded(allowMergeCommits, selectedMergeOptions) + opts.Edits.EnableMergeCommit = &enableMergeCommit + enableSquashMerge := isIncluded(allowSquashMerge, selectedMergeOptions) + opts.Edits.EnableSquashMerge = &enableSquashMerge + enableRebaseMerge := isIncluded(allowRebaseMerge, selectedMergeOptions) + opts.Edits.EnableRebaseMerge = &enableRebaseMerge + if !enableMergeCommit && !enableSquashMerge && !enableRebaseMerge { + return fmt.Errorf("you need to allow at least one merge strategy") + } + + opts.Edits.EnableAutoMerge = &r.AutoMergeAllowed + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Enable Auto Merge?", + Default: r.AutoMergeAllowed, + }, opts.Edits.EnableAutoMerge) + if err != nil { + return err + } + + opts.Edits.DeleteBranchOnMerge = &r.DeleteBranchOnMerge + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Automatically delete head branches after merging?", + Default: r.DeleteBranchOnMerge, + }, opts.Edits.DeleteBranchOnMerge) + if err != nil { + return err + } + case optionTemplateRepo: + opts.Edits.IsTemplate = &r.IsTemplate + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Convert into a template repository?", + Default: r.IsTemplate, + }, opts.Edits.IsTemplate) + if err != nil { + return err + } + case optionAllowForking: + opts.Edits.AllowForking = &r.ForkingAllowed + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Allow forking (of an organization repository)?", + Default: r.ForkingAllowed, + }, opts.Edits.AllowForking) + if err != nil { + return err + } + } + } + return nil } func getTopics(ctx context.Context, httpClient *http.Client, repo ghrepo.Interface) ([]string, error) { @@ -228,3 +498,12 @@ func setTopics(ctx context.Context, httpClient *http.Client, repo ghrepo.Interfa return nil } + +func isIncluded(value string, opts []string) bool { + for _, opt := range opts { + if strings.EqualFold(opt, value) { + return true + } + } + return false +} diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index 9130fbd64..1cf8fd4dd 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -7,6 +7,8 @@ import ( "net/http" "testing" + "github.com/cli/cli/v2/pkg/prompt" + "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -23,11 +25,6 @@ func TestNewCmdEdit(t *testing.T) { wantOpts EditOptions wantErr string }{ - { - name: "no argument", - args: "", - wantErr: "at least one flag is required", - }, { name: "change repo description", args: "--description hello", @@ -135,6 +132,11 @@ func Test_editRun(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(true) + io.SetStdinTTY(true) + io.SetStderrTTY(true) + httpReg := &httpmock.Registry{} defer httpReg.Verify(t) if tt.httpStubs != nil { @@ -143,6 +145,186 @@ func Test_editRun(t *testing.T) { opts := &tt.opts opts.HTTPClient = &http.Client{Transport: httpReg} + opts.IO = io + + err := editRun(context.Background(), opts) + if tt.wantsErr == "" { + require.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantsErr) + return + } + }) + } +} + +func Test_editRun_interactive(t *testing.T) { + tests := []struct { + name string + opts EditOptions + askStubs func(*prompt.AskStubber) + httpStubs func(*testing.T, *httpmock.Registry) + wantsStderr string + wantsErr string + }{ + { + name: "updates repo description", + opts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + InteractiveMode: true, + }, + askStubs: func(as *prompt.AskStubber) { + as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description"}) + as.StubPrompt("Description of the repository").AnswerWith("awesome repo description") + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(` + { + "data": { + "repository": { + "description": "old description", + "homePageUrl": "https://url.com", + "defaultBranchRef": { + "name": "main" + }, + "isInOrganization": false, + "repositoryTopics": { + "nodes": [{ + "topic": { + "name": "x" + } + }] + } + } + } + }`)) + reg.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO"), + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { + assert.Equal(t, "awesome repo description", payload["description"]) + })) + }, + }, + { + name: "updates repo topics", + opts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + InteractiveMode: true, + }, + askStubs: func(as *prompt.AskStubber) { + as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description", "Topics"}) + as.StubPrompt("Description of the repository").AnswerWith("awesome repo description") + as.StubPrompt("Add topics?(csv format)").AnswerWith("a,b,c,d") + as.StubPrompt("Remove Topics").AnswerWith([]string{"x"}) + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(` + { + "data": { + "repository": { + "description": "old description", + "homePageUrl": "https://url.com", + "defaultBranchRef": { + "name": "main" + }, + "isInOrganization": false, + "repositoryTopics": { + "nodes": [{ + "topic": { + "name": "x" + } + }] + } + } + } + }`)) + reg.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO"), + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { + assert.Equal(t, "awesome repo description", payload["description"]) + })) + reg.Register( + httpmock.REST("PUT", "repos/OWNER/REPO/topics"), + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { + assert.Equal(t, []interface{}{"a", "b", "c", "d"}, payload["names"]) + })) + }, + }, + { + name: "updates repo merge options", + opts: EditOptions{ + Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), + InteractiveMode: true, + }, + askStubs: func(as *prompt.AskStubber) { + as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Merge Options"}) + as.StubPrompt("Allowed merge strategies").AnswerWith([]string{allowMergeCommits, allowRebaseMerge}) + as.StubPrompt("Enable Auto Merge?").AnswerWith(false) + as.StubPrompt("Automatically delete head branches after merging?").AnswerWith(false) + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(` + { + "data": { + "repository": { + "description": "old description", + "homePageUrl": "https://url.com", + "defaultBranchRef": { + "name": "main" + }, + "isInOrganization": false, + "squashMergeAllowed": false, + "rebaseMergeAllowed": true, + "mergeCommitAllowed": true, + "deleteBranchOnMerge": false, + "repositoryTopics": { + "nodes": [{ + "topic": { + "name": "x" + } + }] + } + } + } + }`)) + reg.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO"), + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { + assert.Equal(t, true, payload["allow_merge_commit"]) + assert.Equal(t, false, payload["allow_squash_merge"]) + assert.Equal(t, true, payload["allow_rebase_merge"]) + })) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(true) + io.SetStdinTTY(true) + io.SetStderrTTY(true) + + httpReg := &httpmock.Registry{} + defer httpReg.Verify(t) + if tt.httpStubs != nil { + tt.httpStubs(t, httpReg) + } + + opts := &tt.opts + opts.HTTPClient = &http.Client{Transport: httpReg} + opts.IO = io + + as := prompt.NewAskStubber(t) + if tt.askStubs != nil { + tt.askStubs(as) + } err := editRun(context.Background(), opts) if tt.wantsErr == "" { From e6b09b45deb10c3d465b598e5ff91b4d3b22f2a4 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 12:29:49 -0700 Subject: [PATCH 43/75] Fix up error and help language --- pkg/cmd/run/rerun/rerun.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 0303060b4..45f231f90 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -41,7 +41,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm if len(args) == 0 && opts.JobID == "" { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("run or job ID required when not running interactively") + return cmdutil.FlagErrorf("`` or `--job` required when not running interactively") } else { opts.Prompt = true } @@ -50,7 +50,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } if opts.RunID != "" && opts.JobID != "" { - return cmdutil.FlagErrorf("specify only one of or ") + return cmdutil.FlagErrorf("specify only one of `` or `--job`") } if runF != nil { @@ -60,7 +60,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs, including dependencies") cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies") return cmd @@ -107,7 +107,7 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to get runs: %w", err) } if len(runs) == 0 { - return errors.New("no recent runs have failed; please specify a specific run ID") + return errors.New("no recent runs have failed; please specify a specific ``") } runID, err = shared.PromptForRun(cs, runs) if err != nil { From f6d2f83938b54a38441500c9fb34a25fd8a1850a Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 15 Mar 2022 01:44:51 -0400 Subject: [PATCH 44/75] Support setting Dependabot secrets (#5134) * Support setting Dependabot secrets * lint: Remove unnecessary assignment * Use `StringEnumFlag` helper for Application input * Add Dependabot to command description * Move repository name mapping after input validation * Error when multiple secret entities are set * Return an error for invalid apps * Use `assert` instead of `require` --- pkg/cmd/secret/secret.go | 5 +- pkg/cmd/secret/set/http.go | 19 +-- pkg/cmd/secret/set/set.go | 71 ++++++---- pkg/cmd/secret/set/set_test.go | 132 +++++++++++++---- pkg/cmd/secret/shared/shared.go | 83 +++++++++++ pkg/cmd/secret/shared/shared_test.go | 203 +++++++++++++++++++++++++++ 6 files changed, 446 insertions(+), 67 deletions(-) create mode 100644 pkg/cmd/secret/shared/shared_test.go diff --git a/pkg/cmd/secret/secret.go b/pkg/cmd/secret/secret.go index 2ef5eeec6..94231b661 100644 --- a/pkg/cmd/secret/secret.go +++ b/pkg/cmd/secret/secret.go @@ -14,8 +14,9 @@ func NewCmdSecret(f *cmdutil.Factory) *cobra.Command { Use: "secret ", Short: "Manage GitHub secrets", Long: heredoc.Doc(` - Secrets can be set at the repository, environment, or organization level for use in - GitHub Actions. User secrets can be set for use in GitHub Codespaces. + Secrets can be set at the repository, or organization level for use in + GitHub Actions or Dependabot. User secrets can be set for use in GitHub Codespaces. + Environment secrets can be set for use in GitHub Actions. Run "gh help secret set" to learn how to get started. `), } diff --git a/pkg/cmd/secret/set/http.go b/pkg/cmd/secret/set/http.go index f74f757d9..f36c2b59e 100644 --- a/pkg/cmd/secret/set/http.go +++ b/pkg/cmd/secret/set/http.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/secret/shared" ) type SecretPayload struct { @@ -40,17 +41,17 @@ func getPubKey(client *api.Client, host, path string) (*PubKey, error) { return &pk, nil } -func getOrgPublicKey(client *api.Client, host, orgName string) (*PubKey, error) { - return getPubKey(client, host, fmt.Sprintf("orgs/%s/actions/secrets/public-key", orgName)) +func getOrgPublicKey(client *api.Client, host, orgName string, app shared.App) (*PubKey, error) { + return getPubKey(client, host, fmt.Sprintf("orgs/%s/%s/secrets/public-key", orgName, app)) } func getUserPublicKey(client *api.Client, host string) (*PubKey, error) { return getPubKey(client, host, "user/codespaces/secrets/public-key") } -func getRepoPubKey(client *api.Client, repo ghrepo.Interface) (*PubKey, error) { - return getPubKey(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets/public-key", - ghrepo.FullName(repo))) +func getRepoPubKey(client *api.Client, repo ghrepo.Interface, app shared.App) (*PubKey, error) { + return getPubKey(client, repo.RepoHost(), fmt.Sprintf("repos/%s/%s/secrets/public-key", + ghrepo.FullName(repo), app)) } func getEnvPubKey(client *api.Client, repo ghrepo.Interface, envName string) (*PubKey, error) { @@ -68,14 +69,14 @@ func putSecret(client *api.Client, host, path string, payload interface{}) error return client.REST(host, "PUT", path, requestBody, nil) } -func putOrgSecret(client *api.Client, host string, pk *PubKey, orgName, visibility, secretName, eValue string, repositoryIDs []int64) error { +func putOrgSecret(client *api.Client, host string, pk *PubKey, orgName, visibility, secretName, eValue string, repositoryIDs []int64, app shared.App) error { payload := SecretPayload{ EncryptedValue: eValue, KeyID: pk.ID, Repositories: repositoryIDs, Visibility: visibility, } - path := fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, secretName) + path := fmt.Sprintf("orgs/%s/%s/secrets/%s", orgName, app, secretName) return putSecret(client, host, path, payload) } @@ -107,12 +108,12 @@ func putEnvSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, envName return putSecret(client, repo.RepoHost(), path, payload) } -func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string) error { +func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string, app shared.App) error { payload := SecretPayload{ EncryptedValue: eValue, KeyID: pk.ID, } - path := fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(repo), secretName) + path := fmt.Sprintf("repos/%s/%s/secrets/%s", ghrepo.FullName(repo), app, secretName) return putSecret(client, repo.RepoHost(), path, payload) } diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 0c46d5f70..c147a81f5 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -42,6 +42,7 @@ type SetOptions struct { Visibility string RepositoryNames []string EnvFile string + Application string } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -56,9 +57,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command Short: "Create or update secrets", Long: heredoc.Doc(` Set a value for a secret on one of the following levels: - - repository (default): available to Actions runs in a repository + - repository (default): available to Actions runs or Dependabot in a repository - environment: available to Actions runs for a deployment environment in a repository - - organization: available to Actions runs within an organization + - organization: available to Actions runs or Dependabot within an organization - user: available to Codespaces for your user Organization and user secrets can optionally be restricted to only be available to @@ -88,6 +89,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command # Set user-level secret for Codespaces $ gh secret set MYSECRET --user + # Set repository-level secret for Dependabot + $ gh secret set MYSECRET --app dependabot + # Set multiple secrets imported from the ".env" file $ gh secret set -f .env `), @@ -150,6 +154,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The value for the secret (reads from standard input if not specified)") cmd.Flags().BoolVar(&opts.DoNotStore, "no-store", false, "Print the encrypted, base64-encoded value instead of storing it on Github") cmd.Flags().StringVarP(&opts.EnvFile, "env-file", "f", "", "Load secret names and values from a dotenv-formatted `file`") + cmdutil.StringEnumFlag(cmd, &opts.Application, "app", "a", "", []string{shared.Actions, shared.Codespaces, shared.Dependabot}, "Set the application for a secret") return cmd } @@ -189,6 +194,35 @@ func setRun(opts *SetOptions) error { } } + secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) + if err != nil { + return err + } + + secretApp, err := shared.GetSecretApp(opts.Application, secretEntity) + if err != nil { + return err + } + + if !shared.IsSupportedSecretEntity(secretApp, secretEntity) { + return fmt.Errorf("%s secrets are not supported for %s", secretEntity, secretApp) + } + + var pk *PubKey + switch secretEntity { + case shared.Organization: + pk, err = getOrgPublicKey(client, host, orgName, secretApp) + case shared.Environment: + pk, err = getEnvPubKey(client, baseRepo, envName) + case shared.User: + pk, err = getUserPublicKey(client, host) + default: + pk, err = getRepoPubKey(client, baseRepo, secretApp) + } + if err != nil { + return fmt.Errorf("failed to fetch public key: %w", err) + } + type repoNamesResult struct { ids []int64 err error @@ -206,20 +240,6 @@ func setRun(opts *SetOptions) error { } }() - var pk *PubKey - if orgName != "" { - pk, err = getOrgPublicKey(client, host, orgName) - } else if envName != "" { - pk, err = getEnvPubKey(client, baseRepo, envName) - } else if opts.UserSecrets { - pk, err = getUserPublicKey(client, host) - } else { - pk, err = getRepoPubKey(client, baseRepo) - } - if err != nil { - return fmt.Errorf("failed to fetch public key: %w", err) - } - var repositoryIDs []int64 if result := <-repoNamesC; result.err == nil { repositoryIDs = result.ids @@ -232,7 +252,7 @@ func setRun(opts *SetOptions) error { key := secretKey value := secret go func() { - setc <- setSecret(opts, pk, host, client, baseRepo, key, value, repositoryIDs) + setc <- setSecret(opts, pk, host, client, baseRepo, key, value, repositoryIDs, secretApp, secretEntity) }() } @@ -257,7 +277,7 @@ func setRun(opts *SetOptions) error { } else if orgName == "" { target = ghrepo.FullName(baseRepo) } - fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIcon(), result.key, target) + fmt.Fprintf(opts.IO.Out, "%s Set %s secret %s for %s\n", cs.SuccessIcon(), secretApp.Title(), result.key, target) } return err } @@ -268,7 +288,7 @@ type setResult struct { err error } -func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, baseRepo ghrepo.Interface, secretKey string, secret []byte, repositoryIDs []int64) (res setResult) { +func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, baseRepo ghrepo.Interface, secretKey string, secret []byte, repositoryIDs []int64, app shared.App, entity shared.SecretEntity) (res setResult) { orgName := opts.OrgName envName := opts.EnvName res.key = secretKey @@ -297,14 +317,15 @@ func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, ba return } - if orgName != "" { - err = putOrgSecret(client, host, pk, opts.OrgName, opts.Visibility, secretKey, encoded, repositoryIDs) - } else if envName != "" { + switch entity { + case shared.Organization: + err = putOrgSecret(client, host, pk, orgName, opts.Visibility, secretKey, encoded, repositoryIDs, app) + case shared.Environment: err = putEnvSecret(client, pk, baseRepo, envName, secretKey, encoded) - } else if opts.UserSecrets { + case shared.User: err = putUserSecret(client, host, pk, secretKey, encoded, repositoryIDs) - } else { - err = putRepoSecret(client, pk, baseRepo, secretKey, encoded) + default: + err = putRepoSecret(client, pk, baseRepo, secretKey, encoded, app) } if err != nil { res.err = fmt.Errorf("failed to set secret %q: %w", secretKey, err) diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 7843a8b3b..72640a86a 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -143,6 +143,29 @@ func TestNewCmdSet(t *testing.T) { DoNotStore: true, }, }, + { + name: "Dependabot repo", + cli: `cool_secret -b"a secret" --app Dependabot`, + wants: SetOptions{ + SecretName: "cool_secret", + Visibility: shared.Private, + Body: "a secret", + OrgName: "", + Application: "Dependabot", + }, + }, + { + name: "Dependabot org", + cli: "-ocoolOrg -bs -vselected -rcoolRepo cool_secret -aDependabot", + wants: SetOptions{ + SecretName: "cool_secret", + Visibility: shared.Selected, + RepositoryNames: []string{"coolRepo"}, + Body: "s", + OrgName: "coolOrg", + Application: "Dependabot", + }, + }, } for _, tt := range tests { @@ -181,46 +204,81 @@ func TestNewCmdSet(t *testing.T) { assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName) assert.Equal(t, tt.wants.DoNotStore, gotOpts.DoNotStore) assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames) + assert.Equal(t, tt.wants.Application, gotOpts.Application) }) } } func Test_setRun_repo(t *testing.T) { - reg := &httpmock.Registry{} - - reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/secrets/public-key"), - httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="})) - - reg.Register(httpmock.REST("PUT", "repos/owner/repo/actions/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`)) - - io, _, _, _ := iostreams.Test() - - opts := &SetOptions{ - HttpClient: func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil + tests := []struct { + name string + opts *SetOptions + wantApp string + }{ + { + name: "Actions", + opts: &SetOptions{ + Application: "actions", + }, + wantApp: "actions", }, - Config: func() (config.Config, error) { return config.NewBlankConfig(), nil }, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("owner/repo") + { + name: "Dependabot", + opts: &SetOptions{ + Application: "dependabot", + }, + wantApp: "dependabot", + }, + { + name: "defaults to Actions", + opts: &SetOptions{ + Application: "", + }, + wantApp: "actions", }, - IO: io, - SecretName: "cool_secret", - Body: "a secret", - RandomOverride: fakeRandom, } - err := setRun(opts) - assert.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} - reg.Verify(t) + reg.Register(httpmock.REST("GET", fmt.Sprintf("repos/owner/repo/%s/secrets/public-key", tt.wantApp)), + httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="})) - data, err := ioutil.ReadAll(reg.Requests[1].Body) - assert.NoError(t, err) - var payload SecretPayload - err = json.Unmarshal(data, &payload) - assert.NoError(t, err) - assert.Equal(t, payload.KeyID, "123") - assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=") + reg.Register(httpmock.REST("PUT", fmt.Sprintf("repos/owner/repo/%s/secrets/cool_secret", tt.wantApp)), + httpmock.StatusStringResponse(201, `{}`)) + + io, _, _, _ := iostreams.Test() + + opts := &SetOptions{ + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + Config: func() (config.Config, error) { return config.NewBlankConfig(), nil }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + }, + IO: io, + SecretName: "cool_secret", + Body: "a secret", + RandomOverride: fakeRandom, + Application: tt.opts.Application, + } + + err := setRun(opts) + assert.NoError(t, err) + + reg.Verify(t) + + data, err := ioutil.ReadAll(reg.Requests[1].Body) + assert.NoError(t, err) + var payload SecretPayload + err = json.Unmarshal(data, &payload) + assert.NoError(t, err) + assert.Equal(t, payload.KeyID, "123") + assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=") + }) + } } func Test_setRun_env(t *testing.T) { @@ -268,6 +326,7 @@ func Test_setRun_org(t *testing.T) { opts *SetOptions wantVisibility shared.Visibility wantRepositories []int64 + wantApp string }{ { name: "all vis", @@ -275,6 +334,7 @@ func Test_setRun_org(t *testing.T) { OrgName: "UmbrellaCorporation", Visibility: shared.All, }, + wantApp: "actions", }, { name: "selected visibility", @@ -284,6 +344,16 @@ func Test_setRun_org(t *testing.T) { RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"}, }, wantRepositories: []int64{1, 2}, + wantApp: "actions", + }, + { + name: "Dependabot", + opts: &SetOptions{ + OrgName: "UmbrellaCorporation", + Visibility: shared.All, + Application: "dependabot", + }, + wantApp: "dependabot", }, } @@ -294,11 +364,11 @@ func Test_setRun_org(t *testing.T) { orgName := tt.opts.OrgName reg.Register(httpmock.REST("GET", - fmt.Sprintf("orgs/%s/actions/secrets/public-key", orgName)), + fmt.Sprintf("orgs/%s/%s/secrets/public-key", orgName, tt.wantApp)), httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="})) reg.Register(httpmock.REST("PUT", - fmt.Sprintf("orgs/%s/actions/secrets/cool_secret", orgName)), + fmt.Sprintf("orgs/%s/%s/secrets/cool_secret", orgName, tt.wantApp)), httpmock.StatusStringResponse(201, `{}`)) if len(tt.opts.RepositoryNames) > 0 { diff --git a/pkg/cmd/secret/shared/shared.go b/pkg/cmd/secret/shared/shared.go index 4f58dd971..4e5e70617 100644 --- a/pkg/cmd/secret/shared/shared.go +++ b/pkg/cmd/secret/shared/shared.go @@ -1,5 +1,11 @@ package shared +import ( + "errors" + "fmt" + "strings" +) + type Visibility string const ( @@ -7,3 +13,80 @@ const ( Private = "private" Selected = "selected" ) + +type App string + +const ( + Actions = "actions" + Codespaces = "codespaces" + Dependabot = "dependabot" + Unknown = "unknown" +) + +func (app App) String() string { + return string(app) +} + +func (app App) Title() string { + return strings.Title(app.String()) +} + +type SecretEntity string + +const ( + Repository = "repository" + Organization = "organization" + User = "user" + Environment = "environment" +) + +func GetSecretEntity(orgName, envName string, userSecrets bool) (SecretEntity, error) { + orgSet := orgName != "" + envSet := envName != "" + + if orgSet && envSet || orgSet && userSecrets || envSet && userSecrets { + return "", errors.New("cannot specify multiple secret entities") + } + + if orgSet { + return Organization, nil + } + if envSet { + return Environment, nil + } + if userSecrets { + return User, nil + } + return Repository, nil +} + +func GetSecretApp(app string, entity SecretEntity) (App, error) { + switch strings.ToLower(app) { + case Actions: + return Actions, nil + case Codespaces: + return Codespaces, nil + case Dependabot: + return Dependabot, nil + case "": + if entity == User { + return Codespaces, nil + } + return Actions, nil + default: + return Unknown, fmt.Errorf("invalid application: %s", app) + } +} + +func IsSupportedSecretEntity(app App, entity SecretEntity) bool { + switch app { + case Actions: + return entity == Repository || entity == Organization || entity == Environment + case Codespaces: + return entity == User + case Dependabot: + return entity == Repository || entity == Organization + default: + return false + } +} diff --git a/pkg/cmd/secret/shared/shared_test.go b/pkg/cmd/secret/shared/shared_test.go new file mode 100644 index 000000000..b247a85d9 --- /dev/null +++ b/pkg/cmd/secret/shared/shared_test.go @@ -0,0 +1,203 @@ +package shared + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetSecretEntity(t *testing.T) { + tests := []struct { + name string + orgName string + envName string + userSecrets bool + want SecretEntity + wantErr bool + }{ + { + name: "org", + orgName: "myOrg", + want: Organization, + }, + { + name: "env", + envName: "myEnv", + want: Environment, + }, + { + name: "user", + userSecrets: true, + want: User, + }, + { + name: "defaults to repo", + want: Repository, + }, + { + name: "Errors if both org and env are set", + orgName: "myOrg", + envName: "myEnv", + wantErr: true, + }, + { + name: "Errors if both org and user secrets are set", + orgName: "myOrg", + userSecrets: true, + wantErr: true, + }, + { + name: "Errors if both env and user secrets are set", + envName: "myEnv", + userSecrets: true, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entity, err := GetSecretEntity(tt.orgName, tt.envName, tt.userSecrets) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, entity) + } + }) + } +} + +func TestGetSecretApp(t *testing.T) { + tests := []struct { + name string + app string + entity SecretEntity + want App + wantErr bool + }{ + { + name: "Actions", + app: "actions", + want: Actions, + }, + { + name: "Codespaces", + app: "codespaces", + want: Codespaces, + }, + { + name: "Dependabot", + app: "dependabot", + want: Dependabot, + }, + { + name: "Defaults to Actions for repository", + app: "", + entity: Repository, + want: Actions, + }, + { + name: "Defaults to Actions for organization", + app: "", + entity: Organization, + want: Actions, + }, + { + name: "Defaults to Actions for environment", + app: "", + entity: Environment, + want: Actions, + }, + { + name: "Defaults to Codespaces for user", + app: "", + entity: User, + want: Codespaces, + }, + { + name: "Unknown for invalid apps", + app: "invalid", + want: Unknown, + wantErr: true, + }, + { + name: "case insensitive", + app: "ACTIONS", + want: Actions, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app, err := GetSecretApp(tt.app, tt.entity) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, app) + }) + } +} + +func TestIsSupportedSecretEntity(t *testing.T) { + tests := []struct { + name string + app App + supportedEntities []SecretEntity + unsupportedEntities []SecretEntity + }{ + { + name: "Actions", + app: Actions, + supportedEntities: []SecretEntity{ + Repository, + Organization, + Environment, + }, + unsupportedEntities: []SecretEntity{ + User, + Unknown, + }, + }, + { + name: "Codespaces", + app: Codespaces, + supportedEntities: []SecretEntity{ + User, + }, + unsupportedEntities: []SecretEntity{ + Repository, + Organization, + Environment, + Unknown, + }, + }, + { + name: "Dependabot", + app: Dependabot, + supportedEntities: []SecretEntity{ + Repository, + Organization, + }, + unsupportedEntities: []SecretEntity{ + Environment, + User, + Unknown, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, entity := range tt.supportedEntities { + assert.True(t, IsSupportedSecretEntity(tt.app, entity)) + } + + for _, entity := range tt.unsupportedEntities { + assert.False(t, IsSupportedSecretEntity(tt.app, entity)) + } + }) + } +} From 2163c2312adc2fdfc474d74ed032320248005795 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Tue, 15 Mar 2022 05:50:18 +0000 Subject: [PATCH 45/75] review suggestions --- pkg/cmd/codespace/ports_test.go | 56 +++++++++++----------------- pkg/liveshare/port_forwarder_test.go | 10 ++--- pkg/liveshare/ports.go | 15 ++++---- pkg/liveshare/rpc.go | 26 +++++-------- pkg/liveshare/session.go | 2 +- pkg/liveshare/session_test.go | 4 +- 6 files changed, 46 insertions(+), 67 deletions(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index c15e519ee..9cdfff19d 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -33,18 +33,14 @@ func TestPortsUpdateVisibilitySuccess(t *testing.T) { portsData := []liveshare.PortNotification{ { - Success: true, - PortUpdate: liveshare.PortUpdate{ - Port: 80, - ChangeKind: liveshare.PortChangeKindUpdate, - }, + Success: true, + Port: 80, + ChangeKind: liveshare.PortChangeKindUpdate, }, { - Success: true, - PortUpdate: liveshare.PortUpdate{ - Port: 9999, - ChangeKind: liveshare.PortChangeKindUpdate, - }, + Success: true, + Port: 9999, + ChangeKind: liveshare.PortChangeKindUpdate, }, } @@ -74,20 +70,16 @@ func TestPortsUpdateVisibilityFailure403(t *testing.T) { portsData := []liveshare.PortNotification{ { - Success: true, - PortUpdate: liveshare.PortUpdate{ - Port: 80, - ChangeKind: liveshare.PortChangeKindUpdate, - }, + Success: true, + Port: 80, + ChangeKind: liveshare.PortChangeKindUpdate, }, { - Success: false, - PortUpdate: liveshare.PortUpdate{ - Port: 9999, - ChangeKind: liveshare.PortChangeKindUpdate, - ErrorDetail: "test error", - StatusCode: 403, - }, + Success: false, + Port: 9999, + ChangeKind: liveshare.PortChangeKindUpdate, + ErrorDetail: "test error", + StatusCode: 403, }, } @@ -120,19 +112,15 @@ func TestPortsUpdateVisibilityFailure(t *testing.T) { portsData := []liveshare.PortNotification{ { - Success: true, - PortUpdate: liveshare.PortUpdate{ - Port: 80, - ChangeKind: liveshare.PortChangeKindUpdate, - }, + Success: true, + Port: 80, + ChangeKind: liveshare.PortChangeKindUpdate, }, { - Success: false, - PortUpdate: liveshare.PortUpdate{ - Port: 9999, - ChangeKind: liveshare.PortChangeKindUpdate, - ErrorDetail: "test error", - }, + Success: false, + Port: 9999, + ChangeKind: liveshare.PortChangeKindUpdate, + ErrorDetail: "test error", }, } @@ -190,7 +178,7 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev return case conn := <-ch: pd := portsData[i] - _, _ = conn.DispatchCall(context.Background(), eventResponses[i], pd.PortUpdate, nil) + _, _ = conn.DispatchCall(context.Background(), eventResponses[i], pd, nil) } } }() diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index 6281a2f86..c01497c71 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -27,18 +27,18 @@ func TestNewPortForwarder(t *testing.T) { } type portUpdateNotification struct { - PortUpdate + PortNotification conn *jsonrpc2.Conn } func TestPortForwarderStart(t *testing.T) { streamName, streamCondition := "stream-name", "stream-condition" - port := 8000 + const port = 8000 sendNotification := make(chan portUpdateNotification) serverSharing := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { sendNotification <- portUpdateNotification{ - PortUpdate: PortUpdate{ - Port: int(port), + PortNotification: PortNotification{ + Port: port, ChangeKind: PortChangeKindStart, }, conn: conn, @@ -71,7 +71,7 @@ func TestPortForwarderStart(t *testing.T) { go func() { notif := <-sendNotification - _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif.PortUpdate) + _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif) }() done := make(chan error) diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index 6e7097074..11b41f8c0 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -30,13 +30,6 @@ const ( PortChangeKindUpdate PortChangeKind = "update" ) -type PortUpdate struct { - Port int `json:"port"` - ChangeKind PortChangeKind `json:"changeKind"` - ErrorDetail string `json:"errorDetail"` - StatusCode int `json:"statusCode"` -} - // startSharing tells the Live Share host to start sharing the specified port from the container. // The sessionName describes the purpose of the remote port or service. // It returns an identifier that can be used to open an SSH channel to the remote port. @@ -69,14 +62,20 @@ func (s *Session) startSharing(ctx context.Context, sessionName string, port int } type PortNotification struct { - PortUpdate Success bool + // The following are properties sent by the SharingSucceeded/SharingFailed events in the Codespaces agent + Port int `json:"port"` + ChangeKind PortChangeKind `json:"changeKind"` + ErrorDetail string `json:"errorDetail"` + StatusCode int `json:"statusCode"` } // WaitForPortNotification waits for a port notification to be received. It returns the notification // or an error if the notification is not received before the context is cancelled or it fails // to parse the notification. func (s *Session) WaitForPortNotification(ctx context.Context, port int, notifType PortChangeKind) (*PortNotification, error) { + // We use 1-buffered channels and non-blocking sends so that + // no goroutine gets stuck. notificationCh := make(chan *PortNotification, 1) errCh := make(chan error, 1) diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 4d656a326..1078dc426 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -13,17 +13,18 @@ import ( type rpcClient struct { *jsonrpc2.Conn - conn io.ReadWriteCloser - requestHandler *requestHandler + conn io.ReadWriteCloser + handlersMu sync.Mutex + handlers map[string][]*handlerSt } func newRPCClient(conn io.ReadWriteCloser) *rpcClient { - return &rpcClient{conn: conn, requestHandler: newRequestHandler()} + return &rpcClient{conn: conn, handlers: make(map[string][]*handlerSt)} } func (r *rpcClient) connect(ctx context.Context) { stream := jsonrpc2.NewBufferedStream(r.conn, jsonrpc2.VSCodeObjectCodec{}) - r.Conn = jsonrpc2.NewConn(ctx, stream, r.requestHandler) + r.Conn = jsonrpc2.NewConn(ctx, stream, r) } func (r *rpcClient) do(ctx context.Context, method string, args, result interface{}) error { @@ -48,16 +49,7 @@ type handlerSt struct { fn handlerFn } -type requestHandler struct { - handlersMu sync.Mutex - handlers map[string][]*handlerSt -} - -func newRequestHandler() *requestHandler { - return &requestHandler{handlers: make(map[string][]*handlerSt)} -} - -func (r *requestHandler) register(requestType string, fn handlerFn) func() { +func (r *rpcClient) register(requestType string, fn handlerFn) func() { r.handlersMu.Lock() defer r.handlersMu.Unlock() @@ -69,7 +61,7 @@ func (r *requestHandler) register(requestType string, fn handlerFn) func() { } } -func (r *requestHandler) deregister(requestType string, handler *handlerSt) { +func (r *rpcClient) deregister(requestType string, handler *handlerSt) { r.handlersMu.Lock() defer r.handlersMu.Unlock() @@ -88,14 +80,14 @@ func (r *requestHandler) deregister(requestType string, handler *handlerSt) { } } -func (r *requestHandler) getHandlers(requestType string) []*handlerSt { +func (r *rpcClient) getHandlers(requestType string) []*handlerSt { r.handlersMu.Lock() defer r.handlersMu.Unlock() return r.handlers[requestType] } -func (r *requestHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { +func (r *rpcClient) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { for _, handler := range r.getHandlers(req.Method) { go handler.fn(conn, req) } diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index c7e38225f..75acd54bf 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -33,7 +33,7 @@ func (s *Session) Close() error { // registerRequestHandler registers a handler for the given request type with the RPC // server and returns a callback function to deregister the handler func (s *Session) registerRequestHandler(requestType string, h handlerFn) func() { - return s.rpc.requestHandler.register(requestType, h) + return s.rpc.register(requestType, h) } // StartsSSHServer starts an SSH server in the container, installing sshd if necessary, diff --git a/pkg/liveshare/session_test.go b/pkg/liveshare/session_test.go index 85d26eb09..c06a4f06a 100644 --- a/pkg/liveshare/session_test.go +++ b/pkg/liveshare/session_test.go @@ -78,7 +78,7 @@ func TestServerStartSharing(t *testing.T) { return nil, errors.New("browseURL does not match expected") } sendNotification <- portUpdateNotification{ - PortUpdate: PortUpdate{ + PortNotification: PortNotification{ Port: int(port), ChangeKind: PortChangeKindStart, }, @@ -98,7 +98,7 @@ func TestServerStartSharing(t *testing.T) { go func() { notif := <-sendNotification - _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif.PortUpdate) + _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif) }() done := make(chan error) From 06f1f6eb527a34af1222ca03807a9205a17d6e90 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Tue, 15 Mar 2022 05:59:18 +0000 Subject: [PATCH 46/75] add comments --- pkg/liveshare/ports.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/liveshare/ports.go b/pkg/liveshare/ports.go index 11b41f8c0..851fe3190 100644 --- a/pkg/liveshare/ports.go +++ b/pkg/liveshare/ports.go @@ -62,8 +62,8 @@ func (s *Session) startSharing(ctx context.Context, sessionName string, port int } type PortNotification struct { - Success bool - // The following are properties sent by the SharingSucceeded/SharingFailed events in the Codespaces agent + Success bool // Helps us disambiguate between the SharingSucceeded/SharingFailed events + // The following are properties included in the SharingSucceeded/SharingFailed events sent by the server sharing service in the Codespace Port int `json:"port"` ChangeKind PortChangeKind `json:"changeKind"` ErrorDetail string `json:"errorDetail"` From a20e8b7eec4005f3cf3e29f56ded121e90f675c6 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Tue, 15 Mar 2022 09:50:20 -0700 Subject: [PATCH 47/75] Fix test I missed updating --- pkg/cmd/run/rerun/rerun_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index b7bfc1d62..0f7c80997 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -233,7 +233,7 @@ func TestRerun(t *testing.T) { }})) }, wantErr: true, - errOut: "no recent runs have failed; please specify a specific run ID", + errOut: "no recent runs have failed; please specify a specific ``", }, { name: "unrerunnable", From 7d07249150fe1a24b2b49b3ff7c55e2152446a5e Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Tue, 15 Mar 2022 18:22:32 +0000 Subject: [PATCH 48/75] review suggestions --- pkg/cmd/codespace/ports_test.go | 18 +++++-------- pkg/liveshare/rpc.go | 47 ++++++++++++++------------------- pkg/liveshare/session.go | 2 +- pkg/liveshare/test/server.go | 1 + 4 files changed, 29 insertions(+), 39 deletions(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index 9cdfff19d..e094d6c27 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -85,7 +85,7 @@ func TestPortsUpdateVisibilityFailure403(t *testing.T) { err := runUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) if err == nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("runUpdateVisibilityTest succeeded unexpectedly") } if errors.Unwrap(err) != errUpdatePortVisibilityForbidden { @@ -126,7 +126,7 @@ func TestPortsUpdateVisibilityFailure(t *testing.T) { err := runUpdateVisibilityTest(t, portVisibilities, eventResponses, portsData) if err == nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("runUpdateVisibilityTest succeeded unexpectedly") } var expectedErr *ErrUpdatingPortVisibility @@ -147,9 +147,6 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev } const sessionToken = "session-token" - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - ch := make(chan *jsonrpc2.Conn, 1) updateSharedVisibility := func(conn *jsonrpc2.Conn, rpcReq *jsonrpc2.Request) (interface{}, error) { var req []interface{} @@ -170,14 +167,15 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev return fmt.Errorf("unable to create test server: %w", err) } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - var i int - for ; ; i++ { + for i, pd := range portsData { select { case <-ctx.Done(): return case conn := <-ch: - pd := portsData[i] _, _ = conn.DispatchCall(context.Background(), eventResponses[i], pd, nil) } } @@ -210,7 +208,5 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev portArgs = append(portArgs, fmt.Sprintf("%d:%s", pv.number, pv.visibility)) } - err = a.UpdatePortVisibility(ctx, "codespace-name", portArgs) - - return err + return a.UpdatePortVisibility(ctx, "codespace-name", portArgs) } diff --git a/pkg/liveshare/rpc.go b/pkg/liveshare/rpc.go index 1078dc426..639f538c9 100644 --- a/pkg/liveshare/rpc.go +++ b/pkg/liveshare/rpc.go @@ -15,11 +15,11 @@ type rpcClient struct { *jsonrpc2.Conn conn io.ReadWriteCloser handlersMu sync.Mutex - handlers map[string][]*handlerSt + handlers map[string][]*handlerWrapper } func newRPCClient(conn io.ReadWriteCloser) *rpcClient { - return &rpcClient{conn: conn, handlers: make(map[string][]*handlerSt)} + return &rpcClient{conn: conn, handlers: make(map[string][]*handlerWrapper)} } func (r *rpcClient) connect(ctx context.Context) { @@ -43,17 +43,17 @@ func (r *rpcClient) do(ctx context.Context, method string, args, result interfac return waiter.Wait(waitCtx, result) } -type handlerFn func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) +type handler func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) -type handlerSt struct { - fn handlerFn +type handlerWrapper struct { + fn handler } -func (r *rpcClient) register(requestType string, fn handlerFn) func() { +func (r *rpcClient) register(requestType string, fn handler) func() { r.handlersMu.Lock() defer r.handlersMu.Unlock() - h := &handlerSt{fn: fn} + h := &handlerWrapper{fn: fn} r.handlers[requestType] = append(r.handlers[requestType], h) return func() { @@ -61,34 +61,27 @@ func (r *rpcClient) register(requestType string, fn handlerFn) func() { } } -func (r *rpcClient) deregister(requestType string, handler *handlerSt) { +func (r *rpcClient) deregister(requestType string, handler *handlerWrapper) { r.handlersMu.Lock() defer r.handlersMu.Unlock() - if handlers, ok := r.handlers[requestType]; ok { - newHandlers := []*handlerSt{} - for _, h := range handlers { - if h != handler { - newHandlers = append(newHandlers, h) - } - } - r.handlers[requestType] = newHandlers - - if len(r.handlers[requestType]) == 0 { - delete(r.handlers, requestType) + handlers := r.handlers[requestType] + for i, h := range handlers { + if h == handler { + // Swap h with last element and pop. + last := len(handlers) - 1 + handlers[i], handlers[last] = handlers[last], nil + r.handlers[requestType] = handlers[:last] + break } } } -func (r *rpcClient) getHandlers(requestType string) []*handlerSt { - r.handlersMu.Lock() - defer r.handlersMu.Unlock() - - return r.handlers[requestType] -} - func (r *rpcClient) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - for _, handler := range r.getHandlers(req.Method) { + r.handlersMu.Lock() + defer r.handlersMu.Unlock() + + for _, handler := range r.handlers[req.Method] { go handler.fn(conn, req) } } diff --git a/pkg/liveshare/session.go b/pkg/liveshare/session.go index 75acd54bf..1bffa096f 100644 --- a/pkg/liveshare/session.go +++ b/pkg/liveshare/session.go @@ -32,7 +32,7 @@ func (s *Session) Close() error { // registerRequestHandler registers a handler for the given request type with the RPC // server and returns a callback function to deregister the handler -func (s *Session) registerRequestHandler(requestType string, h handlerFn) func() { +func (s *Session) registerRequestHandler(requestType string, h handler) func() { return s.rpc.register(requestType, h) } diff --git a/pkg/liveshare/test/server.go b/pkg/liveshare/test/server.go index 793b714f0..5dd4e56aa 100644 --- a/pkg/liveshare/test/server.go +++ b/pkg/liveshare/test/server.go @@ -86,6 +86,7 @@ func WithPassword(password string) ServerOption { } } +// WithNonSecure configures the Server as non-secure. func WithNonSecure() ServerOption { return func(s *Server) error { s.nonSecure = true From 1b50852b2dfecd17ca5d3a2a12eb5c16df9fe46b Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Tue, 15 Mar 2022 18:25:54 +0000 Subject: [PATCH 49/75] remove unnecessary new context --- pkg/cmd/codespace/ports_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index e094d6c27..83744d558 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -176,7 +176,7 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev case <-ctx.Done(): return case conn := <-ch: - _, _ = conn.DispatchCall(context.Background(), eventResponses[i], pd, nil) + _, _ = conn.DispatchCall(ctx, eventResponses[i], pd, nil) } } }() From d60419b2db0809f3f760252afcb87760ab63767c Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Tue, 15 Mar 2022 21:07:28 +0000 Subject: [PATCH 50/75] req is no longer needed --- pkg/cmd/codespace/ports_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go index 83744d558..441b6f548 100644 --- a/pkg/cmd/codespace/ports_test.go +++ b/pkg/cmd/codespace/ports_test.go @@ -2,7 +2,6 @@ package codespace import ( "context" - "encoding/json" "errors" "fmt" "testing" @@ -149,11 +148,6 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev ch := make(chan *jsonrpc2.Conn, 1) updateSharedVisibility := func(conn *jsonrpc2.Conn, rpcReq *jsonrpc2.Request) (interface{}, error) { - var req []interface{} - if err := json.Unmarshal(*rpcReq.Params, &req); err != nil { - return nil, fmt.Errorf("unmarshal req: %w", err) - } - ch <- conn return nil, nil } From 3f65e5ae24c296423825a3d1ab557086ec84f4a2 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 11:46:36 -0400 Subject: [PATCH 51/75] Add pending opertion and reason to codespace response struct --- internal/codespaces/api/api.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index d8807ec74..e8cd0ac1f 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -168,17 +168,19 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error // Codespace represents a codespace. type Codespace struct { - Name string `json:"name"` - CreatedAt string `json:"created_at"` - DisplayName string `json:"display_name"` - LastUsedAt string `json:"last_used_at"` - Owner User `json:"owner"` - Repository Repository `json:"repository"` - State string `json:"state"` - GitStatus CodespaceGitStatus `json:"git_status"` - Connection CodespaceConnection `json:"connection"` - Machine CodespaceMachine `json:"machine"` - VSCSTarget string `json:"vscs_target"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + DisplayName string `json:"display_name"` + LastUsedAt string `json:"last_used_at"` + Owner User `json:"owner"` + Repository Repository `json:"repository"` + State string `json:"state"` + GitStatus CodespaceGitStatus `json:"git_status"` + Connection CodespaceConnection `json:"connection"` + Machine CodespaceMachine `json:"machine"` + VSCSTarget string `json:"vscs_target"` + PendingOperation bool `json:"pending_operation"` + PendingOperationDisabledReason string `json:"pending_operation_disabled_reason"` } type CodespaceGitStatus struct { From 3d28c521044a39594683989f96bc4dd98f5fb910 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 11:47:10 -0400 Subject: [PATCH 52/75] Mark codespace with pending op as disabled with reason instead of state --- pkg/cmd/codespace/list.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 17c7b6414..43eeb74e4 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -89,10 +89,22 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget) - tp.AddField(formattedName, nil, cs.Yellow) + var nameColor func(string) string + switch c.PendingOperation { + case false: + nameColor = cs.Yellow + case true: + nameColor = cs.Gray + } + + tp.AddField(formattedName, nil, nameColor) tp.AddField(c.Repository.FullName, nil, nil) tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan) - tp.AddField(c.State, nil, stateColor) + if c.PendingOperation { + tp.AddField(c.PendingOperationDisabledReason, nil, cs.Gray) + } else { + tp.AddField(c.State, nil, stateColor) + } if tp.IsTTY() { ct, err := time.Parse(time.RFC3339, c.CreatedAt) From afa71c4b2fa7c99f0267a35046e8419c8a72db06 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 12:21:46 -0400 Subject: [PATCH 53/75] Disallow ssh'ing to codespace with a pending operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the API already disallows this, this makes the error cleaner and more explicit when a user is trying to start/ssh into a codespace that has a pending operation. Example of the old error message: ``` $ gh cs ssh -c cwndrws-redacted Starting codespace ⣽error connecting to codespace: error starting codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted/start) exit status 1 ``` Example of the new error message: ``` $ gh cs ssh -c cwndrws-redacted codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- pkg/cmd/codespace/ssh.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 6ce783f2f..af6c86614 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -128,6 +128,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e return fmt.Errorf("get or choose codespace: %w", err) } + if codespace.PendingOperation { + return fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + liveshareLogger := noopLogger() if opts.debug { debugLogger, err := newFileLogger(opts.debugFile) From 599c7c900f69642e0927011227ac8d683179cf20 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 12:27:24 -0400 Subject: [PATCH 54/75] Disallow getting logs from codespaces with pending ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the API already disallows this, this pretty much just cleans up the error reporting to the user. Example of old error: ``` $ gh cs logs -c cwndrws-redacted Starting codespace ⣽connecting to codespace: error starting codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted/start) exit status 1 ``` Example of new error: ``` $ gh cs logs -c cwndrws-redacted codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- pkg/cmd/codespace/logs.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index d0a0c233b..221d95215 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -41,6 +41,13 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err return fmt.Errorf("get or choose codespace: %w", err) } + if codespace.PendingOperation { + return fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + authkeys := make(chan error, 1) go func() { authkeys <- checkAuthorizedKeys(ctx, a.apiClient) From da99a1b59bc6f13aa7246ccf8814de8528bb172b Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 14:11:33 -0400 Subject: [PATCH 55/75] Ensure codespace exists and doesn't have a pending op when opening Code The initial intention for this change was to disallow users to open a codespace in VS Code if the codespace has a pending operation. This also adds a side-benefit of presenting the user an error before waiting for VS Code to open if they provide an invalid codespace to open. --- pkg/cmd/codespace/code.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index f80e32527..7b52db29f 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -31,18 +31,19 @@ func newCodeCmd(app *App) *cobra.Command { // VSCode opens a codespace in the local VS VSCode application. func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error { - if codespaceName == "" { - codespace, err := chooseCodespace(ctx, a.apiClient) - if err != nil { - if err == errNoCodespaces { - return err - } - return fmt.Errorf("error choosing codespace: %w", err) - } - codespaceName = codespace.Name + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + return fmt.Errorf("get or choose codespace: %w", err) } - url := vscodeProtocolURL(codespaceName, useInsiders) + if codespace.PendingOperation { + return fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + + url := vscodeProtocolURL(codespace.Name, useInsiders) if err := a.browser.Browse(url); err != nil { return fmt.Errorf("error opening Visual Studio Code: %w", err) } From 5ffe838dce24b5456e804e76a349bdcc70f68eea Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 15:24:35 -0400 Subject: [PATCH 56/75] Disallow any port operations when codespace has pending operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since all of the port operations require the codespace to be running, we need to disallow these operations when there's a pending op since we can't start the codespace in this state. Since the API already disallows this, this is basically cleaning up the error messages that the user sees in this state Old error message: ``` $ gh cs ports forward 80:80 ? Choose codespace: redacted Starting codespace ⣻error connecting to codespace: error starting codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted/start) ``` New error message: ``` $ gh cs ports forward 80:80 ? Choose codespace: redacted codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- pkg/cmd/codespace/ports.go | 42 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 094833e30..cc4365742 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -46,13 +46,9 @@ func newPortsCmd(app *App) *cobra.Command { // ListPorts lists known ports in a codespace. func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) { - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) if err != nil { - // TODO(josebalius): remove special handling of this error here and it other places - if err == errNoCodespaces { - return err - } - return fmt.Errorf("error choosing codespace: %w", err) + return err } devContainerCh := getDevContainer(ctx, a.apiClient, codespace) @@ -235,12 +231,9 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return fmt.Errorf("error parsing port arguments: %w", err) } - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) if err != nil { - if err == errNoCodespaces { - return err - } - return fmt.Errorf("error getting codespace: %w", err) + return err } session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace) @@ -311,12 +304,9 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st return fmt.Errorf("get port pairs: %w", err) } - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) if err != nil { - if err == errNoCodespaces { - return err - } - return fmt.Errorf("error getting codespace: %w", err) + return err } session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace) @@ -380,3 +370,23 @@ func normalizeJSON(j []byte) []byte { // remove trailing commas return bytes.ReplaceAll(j, []byte("},}"), []byte("}}")) } + +func getCodespaceForPorts(ctx context.Context, apiClient apiClient, codespaceName string) (*api.Codespace, error) { + codespace, err := getOrChooseCodespace(ctx, apiClient, codespaceName) + if err != nil { + // TODO(josebalius): remove special handling of this error here and it other places + if err == errNoCodespaces { + return nil, err + } + return nil, fmt.Errorf("error choosing codespace: %w", err) + } + + if codespace.PendingOperation { + return nil, fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + + return codespace, nil +} From 27a5512b41aa4d000b1e62377332c40939d931a5 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 16:58:50 -0400 Subject: [PATCH 57/75] Add test for disallowing ssh when codespace has a pending op --- pkg/cmd/codespace/ssh_test.go | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 pkg/cmd/codespace/ssh_test.go diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go new file mode 100644 index 000000000..3b242ae4b --- /dev/null +++ b/pkg/cmd/codespace/ssh_test.go @@ -0,0 +1,47 @@ +package codespace + +import ( + "context" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" +) + +func TestPendingOperationDisallowsSSH(t *testing.T) { + app := testingSSHApp() + + if err := app.SSH(context.Background(), []string{}, sshOptions{codespace: "disabledCodespace"}); err != nil { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("expected pending operation error, but got: %v", err) + } + } else { + t.Error("expected pending operation error, but got nothing") + } +} + +func testingSSHApp() *App { + user := &api.User{Login: "monalisa"} + disabledCodespace := &api.Codespace{ + Name: "disabledCodespace", + PendingOperation: true, + PendingOperationDisabledReason: "Some pending operation", + } + apiMock := &apiClientMock{ + GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) { + if name == "disabledCodespace" { + return disabledCodespace, nil + } + return nil, nil + }, + GetUserFunc: func(_ context.Context) (*api.User, error) { + return user, nil + }, + AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) { + return []byte{}, nil + }, + } + + io, _, _, _ := iostreams.Test() + return NewApp(io, nil, apiMock, nil) +} From 6346779f3521090e053bf58c7decfb47c9ff243e Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 17:03:54 -0400 Subject: [PATCH 58/75] Add test for disallowing logs when codespace has a pending op --- pkg/cmd/codespace/logs_test.go | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 pkg/cmd/codespace/logs_test.go diff --git a/pkg/cmd/codespace/logs_test.go b/pkg/cmd/codespace/logs_test.go new file mode 100644 index 000000000..1eaf2e9d9 --- /dev/null +++ b/pkg/cmd/codespace/logs_test.go @@ -0,0 +1,47 @@ +package codespace + +import ( + "context" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" +) + +func TestPendingOperationDisallowsLogs(t *testing.T) { + app := testingLogsApp() + + if err := app.Logs(context.Background(), "disabledCodespace", false); err != nil { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("expected pending operation error, but got: %v", err) + } + } else { + t.Error("expected pending operation error, but got nothing") + } +} + +func testingLogsApp() *App { + user := &api.User{Login: "monalisa"} + disabledCodespace := &api.Codespace{ + Name: "disabledCodespace", + PendingOperation: true, + PendingOperationDisabledReason: "Some pending operation", + } + apiMock := &apiClientMock{ + GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) { + if name == "disabledCodespace" { + return disabledCodespace, nil + } + return nil, nil + }, + GetUserFunc: func(_ context.Context) (*api.User, error) { + return user, nil + }, + AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) { + return []byte{}, nil + }, + } + + io, _, _, _ := iostreams.Test() + return NewApp(io, nil, apiMock, nil) +} From f94a1a2bd49b7e6c7b747032b43214f45a4c7d45 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 17:12:16 -0400 Subject: [PATCH 59/75] Add test for disallowing opening vs code for codespace with pending op I also refactored the existing vs code test a bit to work with the new use of `getOrChooseCodespace`. --- pkg/cmd/codespace/code_test.go | 48 +++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index cbc59864a..e35060c73 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -4,7 +4,9 @@ import ( "context" "testing" + "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" ) func TestApp_VSCode(t *testing.T) { @@ -41,7 +43,8 @@ func TestApp_VSCode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { b := &cmdutil.TestBrowser{} a := &App{ - browser: b, + browser: b, + apiClient: testCodeApiMock(), } if err := a.VSCode(context.Background(), tt.args.codespaceName, tt.args.useInsiders); (err != nil) != tt.wantErr { t.Errorf("App.VSCode() error = %v, wantErr %v", err, tt.wantErr) @@ -50,3 +53,46 @@ func TestApp_VSCode(t *testing.T) { }) } } + +func TestPendingOperationDisallowsCode(t *testing.T) { + app := testingLogsApp() + + if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("expected pending operation error, but got: %v", err) + } + } else { + t.Error("expected pending operation error, but got nothing") + } +} + +func testingCodeApp() *App { + io, _, _, _ := iostreams.Test() + return NewApp(io, nil, testCodeApiMock(), nil) +} + +func testCodeApiMock() *apiClientMock { + user := &api.User{Login: "monalisa"} + testingCodespace := &api.Codespace{ + Name: "monalisa-cli-cli-abcdef", + } + disabledCodespace := &api.Codespace{ + Name: "disabledCodespace", + PendingOperation: true, + PendingOperationDisabledReason: "Some pending operation", + } + return &apiClientMock{ + GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) { + if name == "disabledCodespace" { + return disabledCodespace, nil + } + return testingCodespace, nil + }, + GetUserFunc: func(_ context.Context) (*api.User, error) { + return user, nil + }, + AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) { + return []byte{}, nil + }, + } +} From 3ed2e49bd95f324abbd01d09a72b7ab7d8274d97 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 17:17:57 -0400 Subject: [PATCH 60/75] Add tests for disallowing all port commands for codespace w/ pending op --- pkg/cmd/codespace/ports_test.go | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 pkg/cmd/codespace/ports_test.go diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go new file mode 100644 index 000000000..f1487ed2e --- /dev/null +++ b/pkg/cmd/codespace/ports_test.go @@ -0,0 +1,71 @@ +package codespace + +import ( + "context" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" +) + +func TestPendingOperationDisallowsListPorts(t *testing.T) { + app := testingPortsApp() + + if err := app.ListPorts(context.Background(), "disabledCodespace", nil); err != nil { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("expected pending operation error, but got: %v", err) + } + } else { + t.Error("expected pending operation error, but got nothing") + } +} + +func TestPendingOperationDisallowsUpdatePortVisability(t *testing.T) { + app := testingPortsApp() + + if err := app.UpdatePortVisibility(context.Background(), "disabledCodespace", nil); err != nil { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("expected pending operation error, but got: %v", err) + } + } else { + t.Error("expected pending operation error, but got nothing") + } +} + +func TestPendingOperationDisallowsForwardPorts(t *testing.T) { + app := testingPortsApp() + + if err := app.ForwardPorts(context.Background(), "disabledCodespace", nil); err != nil { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("expected pending operation error, but got: %v", err) + } + } else { + t.Error("expected pending operation error, but got nothing") + } +} + +func testingPortsApp() *App { + user := &api.User{Login: "monalisa"} + disabledCodespace := &api.Codespace{ + Name: "disabledCodespace", + PendingOperation: true, + PendingOperationDisabledReason: "Some pending operation", + } + apiMock := &apiClientMock{ + GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) { + if name == "disabledCodespace" { + return disabledCodespace, nil + } + return nil, nil + }, + GetUserFunc: func(_ context.Context) (*api.User, error) { + return user, nil + }, + AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) { + return []byte{}, nil + }, + } + + io, _, _, _ := iostreams.Test() + return NewApp(io, nil, apiMock, nil) +} From 10e43b51364de4cad6d6d79bed3dd9d493bc86f2 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 08:43:34 -0400 Subject: [PATCH 61/75] Use color variable instead of literal for disabled reason --- pkg/cmd/codespace/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 43eeb74e4..41f44d299 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -101,7 +101,7 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField(c.Repository.FullName, nil, nil) tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan) if c.PendingOperation { - tp.AddField(c.PendingOperationDisabledReason, nil, cs.Gray) + tp.AddField(c.PendingOperationDisabledReason, nil, nameColor) } else { tp.AddField(c.State, nil, stateColor) } From 8bf0cb8f13bafff98b6be64db208542ac4d65eb2 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 08:51:33 -0400 Subject: [PATCH 62/75] Refactor the getOrChooseCodespace to always check for pending ops --- pkg/cmd/codespace/code.go | 7 ------- pkg/cmd/codespace/code_test.go | 2 +- pkg/cmd/codespace/common.go | 7 +++++++ pkg/cmd/codespace/logs.go | 7 ------- pkg/cmd/codespace/logs_test.go | 2 +- pkg/cmd/codespace/ports.go | 26 +++----------------------- pkg/cmd/codespace/ssh.go | 7 ------- pkg/cmd/codespace/ssh_test.go | 2 +- 8 files changed, 13 insertions(+), 47 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index 7b52db29f..ba2455385 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -36,13 +36,6 @@ func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool return fmt.Errorf("get or choose codespace: %w", err) } - if codespace.PendingOperation { - return fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - url := vscodeProtocolURL(codespace.Name, useInsiders) if err := a.browser.Browse(url); err != nil { return fmt.Errorf("error opening Visual Studio Code: %w", err) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index e35060c73..0eb3b7ade 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -58,7 +58,7 @@ func TestPendingOperationDisallowsCode(t *testing.T) { app := testingLogsApp() if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil { - if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go index 6061eac78..aec386f6c 100644 --- a/pkg/cmd/codespace/common.go +++ b/pkg/cmd/codespace/common.go @@ -183,6 +183,13 @@ func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceNam } } + if codespace.PendingOperation { + return nil, fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + return codespace, nil } diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index 221d95215..d0a0c233b 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -41,13 +41,6 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err return fmt.Errorf("get or choose codespace: %w", err) } - if codespace.PendingOperation { - return fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - authkeys := make(chan error, 1) go func() { authkeys <- checkAuthorizedKeys(ctx, a.apiClient) diff --git a/pkg/cmd/codespace/logs_test.go b/pkg/cmd/codespace/logs_test.go index 1eaf2e9d9..225e10680 100644 --- a/pkg/cmd/codespace/logs_test.go +++ b/pkg/cmd/codespace/logs_test.go @@ -12,7 +12,7 @@ func TestPendingOperationDisallowsLogs(t *testing.T) { app := testingLogsApp() if err := app.Logs(context.Background(), "disabledCodespace", false); err != nil { - if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index cc4365742..274be8d7b 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -46,7 +46,7 @@ func newPortsCmd(app *App) *cobra.Command { // ListPorts lists known ports in a codespace. func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) { - codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { return err } @@ -231,7 +231,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return fmt.Errorf("error parsing port arguments: %w", err) } - codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { return err } @@ -304,7 +304,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st return fmt.Errorf("get port pairs: %w", err) } - codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { return err } @@ -370,23 +370,3 @@ func normalizeJSON(j []byte) []byte { // remove trailing commas return bytes.ReplaceAll(j, []byte("},}"), []byte("}}")) } - -func getCodespaceForPorts(ctx context.Context, apiClient apiClient, codespaceName string) (*api.Codespace, error) { - codespace, err := getOrChooseCodespace(ctx, apiClient, codespaceName) - if err != nil { - // TODO(josebalius): remove special handling of this error here and it other places - if err == errNoCodespaces { - return nil, err - } - return nil, fmt.Errorf("error choosing codespace: %w", err) - } - - if codespace.PendingOperation { - return nil, fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - - return codespace, nil -} diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index af6c86614..6ce783f2f 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -128,13 +128,6 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e return fmt.Errorf("get or choose codespace: %w", err) } - if codespace.PendingOperation { - return fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - liveshareLogger := noopLogger() if opts.debug { debugLogger, err := newFileLogger(opts.debugFile) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 3b242ae4b..fac5147bb 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -12,7 +12,7 @@ func TestPendingOperationDisallowsSSH(t *testing.T) { app := testingSSHApp() if err := app.SSH(context.Background(), []string{}, sshOptions{codespace: "disabledCodespace"}); err != nil { - if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { From a2f76fdfe0e92e403ff301d668a96939c997b80b Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 09:10:54 -0400 Subject: [PATCH 63/75] Fix copy pasta error to appease the linter --- pkg/cmd/codespace/code_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index 0eb3b7ade..eda616a20 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -55,7 +55,7 @@ func TestApp_VSCode(t *testing.T) { } func TestPendingOperationDisallowsCode(t *testing.T) { - app := testingLogsApp() + app := testingCodeApp() if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil { if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { From 64eecef17636627abbc20ff07ac09fcd1244637b Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 09:36:14 -0400 Subject: [PATCH 64/75] Remove unhelpful error wrapper --- pkg/cmd/codespace/code.go | 2 +- pkg/cmd/codespace/code_test.go | 2 +- pkg/cmd/codespace/logs.go | 2 +- pkg/cmd/codespace/logs_test.go | 2 +- pkg/cmd/codespace/ssh.go | 2 +- pkg/cmd/codespace/ssh_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index ba2455385..0942d15d3 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -33,7 +33,7 @@ func newCodeCmd(app *App) *cobra.Command { func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error { codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { - return fmt.Errorf("get or choose codespace: %w", err) + return err } url := vscodeProtocolURL(codespace.Name, useInsiders) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index eda616a20..30f06bd39 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -58,7 +58,7 @@ func TestPendingOperationDisallowsCode(t *testing.T) { app := testingCodeApp() if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil { - if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index d0a0c233b..6feab3080 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -38,7 +38,7 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { - return fmt.Errorf("get or choose codespace: %w", err) + return err } authkeys := make(chan error, 1) diff --git a/pkg/cmd/codespace/logs_test.go b/pkg/cmd/codespace/logs_test.go index 225e10680..1eaf2e9d9 100644 --- a/pkg/cmd/codespace/logs_test.go +++ b/pkg/cmd/codespace/logs_test.go @@ -12,7 +12,7 @@ func TestPendingOperationDisallowsLogs(t *testing.T) { app := testingLogsApp() if err := app.Logs(context.Background(), "disabledCodespace", false); err != nil { - if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 6ce783f2f..dfbc75694 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -125,7 +125,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace) if err != nil { - return fmt.Errorf("get or choose codespace: %w", err) + return err } liveshareLogger := noopLogger() diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index fac5147bb..3b242ae4b 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -12,7 +12,7 @@ func TestPendingOperationDisallowsSSH(t *testing.T) { app := testingSSHApp() if err := app.SSH(context.Background(), []string{}, sshOptions{codespace: "disabledCodespace"}); err != nil { - if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { From 7ca31e02b4feb25c903b7811fec0745815742320 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 10:13:46 -0400 Subject: [PATCH 65/75] Disallow editing a codespace that has a pending operation This is already prevented by the API, but this will make the error message cleaner and easier to understand for the user. Example of old error message: ``` $ gh cs edit -c cwndrws-redacted -d "some silly name" error editing codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted) exit status 1 ``` Example of new error message: ``` $ gh cs edit -c cwndrws-redacted -d "some silly name" error editing codespace: codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- internal/codespaces/api/api.go | 22 ++++++++++++++++ internal/codespaces/api/api_test.go | 39 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index e8cd0ac1f..e9ef4c339 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -783,6 +783,20 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E defer resp.Body.Close() if resp.StatusCode != http.StatusOK { + // 422 (unprocessable entity) is likely caused by the codespace having a + // pending op, so we'll fetch the codespace to see if that's the case + // and return a more understandable error message. + if resp.StatusCode == http.StatusUnprocessableEntity { + pendingOp, reason, err := a.checkForPendingOperation(ctx, codespaceName) + // If there's an error or there's not a pending op, we want to let + // this fall through to the normal api.HandleHTTPError flow + if err == nil && pendingOp { + return nil, fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + reason, + ) + } + } return nil, api.HandleHTTPError(resp) } @@ -799,6 +813,14 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E return &response, nil } +func (a *API) checkForPendingOperation(ctx context.Context, codespaceName string) (bool, string, error) { + codespace, err := a.GetCodespace(ctx, codespaceName, false) + if err != nil { + return false, "", err + } + return codespace.PendingOperation, codespace.PendingOperationDisabledReason, nil +} + type getCodespaceRepositoryContentsResponse struct { Content string `json:"content"` } diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go index ebbbe5209..81eb563c3 100644 --- a/internal/codespaces/api/api_test.go +++ b/internal/codespaces/api/api_test.go @@ -388,6 +388,7 @@ func createFakeEditServer(t *testing.T, codespaceName string) *httptest.Server { fmt.Fprint(w, string(responseData)) })) } + func TestAPI_EditCodespace(t *testing.T) { type args struct { ctx context.Context @@ -434,3 +435,41 @@ func TestAPI_EditCodespace(t *testing.T) { }) } } + +func createFakeEditPendingOpServer(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPatch { + w.WriteHeader(http.StatusUnprocessableEntity) + return + } + + if r.Method == http.MethodGet { + response := Codespace{ + PendingOperation: true, + PendingOperationDisabledReason: "Some pending operation", + } + + responseData, _ := json.Marshal(response) + fmt.Fprint(w, string(responseData)) + return + } + })) +} + +func TestAPI_EditCodespacePendingOperation(t *testing.T) { + svr := createFakeEditPendingOpServer(t) + defer svr.Close() + + a := &API{ + client: &http.Client{}, + githubAPI: svr.URL, + } + + _, err := a.EditCodespace(context.Background(), "disabledCodespace", &EditCodespaceParams{DisplayName: "some silly name"}) + if err == nil { + t.Error("Expected pending operation error, but got nothing") + } + if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { + t.Errorf("Expected pending operation error, but got %v", err) + } +} From 4eedfc05c1f672aeb0f75f29eb5bb0c762abb8be Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 10:20:13 -0400 Subject: [PATCH 66/75] Add docs link comment to Codespaces struct definition --- internal/codespaces/api/api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index e9ef4c339..e8c76e6be 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -167,6 +167,8 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error } // Codespace represents a codespace. +// You can see more about the fields in this type in the codespaces api docs: +// https://docs.github.com/en/rest/reference/codespaces type Codespace struct { Name string `json:"name"` CreatedAt string `json:"created_at"` From 311b9bd38042be530bffe4c119eb8e6dd8e1f531 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:25:15 +0000 Subject: [PATCH 67/75] Bump github.com/stretchr/testify from 1.7.0 to 1.7.1 Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index cd63fd72b..e34008e52 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/sourcegraph/jsonrpc2 v0.1.0 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c diff --git a/go.sum b/go.sum index f51a48221..5ad634436 100644 --- a/go.sum +++ b/go.sum @@ -224,8 +224,9 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From c70e2a345e403f4642bfef5223c69f6d10deb563 Mon Sep 17 00:00:00 2001 From: Bernardo <68619889+bchuecos@users.noreply.github.com> Date: Wed, 16 Mar 2022 20:34:51 +0000 Subject: [PATCH 68/75] allow 2 go routines to send --- pkg/liveshare/port_forwarder_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index c01497c71..1a4526f2a 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -36,6 +36,7 @@ func TestPortForwarderStart(t *testing.T) { const port = 8000 sendNotification := make(chan portUpdateNotification) serverSharing := func(conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) { + // Send the PortNotification that will be awaited on in session.StartSharing sendNotification <- portUpdateNotification{ PortNotification: PortNotification{ Port: port, @@ -74,7 +75,7 @@ func TestPortForwarderStart(t *testing.T) { _, _ = notif.conn.DispatchCall(context.Background(), "serverSharing.sharingSucceeded", notif) }() - done := make(chan error) + done := make(chan error, 2) go func() { done <- NewPortForwarder(session, "ssh", port, false).ForwardToListener(ctx, listen) }() @@ -88,16 +89,20 @@ func TestPortForwarderStart(t *testing.T) { } if conn == nil { done <- errors.New("failed to connect to forwarded port") + return } b := make([]byte, len("stream-data")) if _, err := conn.Read(b); err != nil && err != io.EOF { done <- fmt.Errorf("reading stream: %w", err) + return } if string(b) != "stream-data" { done <- fmt.Errorf("stream data is not expected value, got: %s", string(b)) + return } if _, err := conn.Write([]byte("new-data")); err != nil { done <- fmt.Errorf("writing to stream: %w", err) + return } done <- nil }() From 31c6ba3807e6be436530d747715193ca0d69d1ef Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 17 Mar 2022 05:42:12 -0400 Subject: [PATCH 69/75] Support listing and removing Dependabot secrets (#5160) * Support listing and removing Dependabot secrets * Remove duplicated tests --- pkg/cmd/secret/list/list.go | 48 +++++++---- pkg/cmd/secret/list/list_test.go | 69 ++++++++++++++++ pkg/cmd/secret/remove/remove.go | 48 ++++++++--- pkg/cmd/secret/remove/remove_test.go | 119 ++++++++++++++++++--------- 4 files changed, 217 insertions(+), 67 deletions(-) diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index e197ad748..2ccc4e221 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -29,6 +29,7 @@ type ListOptions struct { OrgName string EnvName string UserSecrets bool + Application string } func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { @@ -43,9 +44,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Short: "List secrets", Long: heredoc.Doc(` List secrets on one of the following levels: - - repository (default): available to Actions runs in a repository + - repository (default): available to Actions runs or Dependabot in a repository - environment: available to Actions runs for a deployment environment in a repository - - organization: available to Actions runs within an organization + - organization: available to Actions runs or Dependabot within an organization - user: available to Codespaces for your user `), Aliases: []string{"ls"}, @@ -69,6 +70,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization") cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List secrets for an environment") cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "List a secret for your user") + cmdutil.StringEnumFlag(cmd, &opts.Application, "app", "a", "", []string{shared.Actions, shared.Codespaces, shared.Dependabot}, "List secrets for a specific application") return cmd } @@ -90,15 +92,29 @@ func listRun(opts *ListOptions) error { } } + secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) + if err != nil { + return err + } + + secretApp, err := shared.GetSecretApp(opts.Application, secretEntity) + if err != nil { + return err + } + + if !shared.IsSupportedSecretEntity(secretApp, secretEntity) { + return fmt.Errorf("%s secrets are not supported for %s", secretEntity, secretApp) + } + var secrets []*Secret showSelectedRepoInfo := opts.IO.IsStdoutTTY() - if orgName == "" && !opts.UserSecrets { - if envName == "" { - secrets, err = getRepoSecrets(client, baseRepo) - } else { - secrets, err = getEnvSecrets(client, baseRepo, envName) - } - } else { + + switch secretEntity { + case shared.Repository: + secrets, err = getRepoSecrets(client, baseRepo, secretApp) + case shared.Environment: + secrets, err = getEnvSecrets(client, baseRepo, envName) + case shared.Organization, shared.User: var cfg config.Config var host string @@ -112,10 +128,10 @@ func listRun(opts *ListOptions) error { return err } - if opts.UserSecrets { + if secretEntity == shared.User { secrets, err = getUserSecrets(client, host, showSelectedRepoInfo) } else { - secrets, err = getOrgSecrets(client, host, orgName, showSelectedRepoInfo) + secrets, err = getOrgSecrets(client, host, orgName, showSelectedRepoInfo, secretApp) } } @@ -179,8 +195,8 @@ func fmtVisibility(s Secret) string { return "" } -func getOrgSecrets(client httpClient, host, orgName string, showSelectedRepoInfo bool) ([]*Secret, error) { - secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/actions/secrets", orgName)) +func getOrgSecrets(client httpClient, host, orgName string, showSelectedRepoInfo bool, app shared.App) ([]*Secret, error) { + secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/%s/secrets", orgName, app)) if err != nil { return nil, err } @@ -215,9 +231,9 @@ func getEnvSecrets(client httpClient, repo ghrepo.Interface, envName string) ([] return getSecrets(client, repo.RepoHost(), path) } -func getRepoSecrets(client httpClient, repo ghrepo.Interface) ([]*Secret, error) { - return getSecrets(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets", - ghrepo.FullName(repo))) +func getRepoSecrets(client httpClient, repo ghrepo.Interface, app shared.App) ([]*Secret, error) { + return getSecrets(client, repo.RepoHost(), fmt.Sprintf("repos/%s/%s/secrets", + ghrepo.FullName(repo), app)) } type secretsPayload struct { diff --git a/pkg/cmd/secret/list/list_test.go b/pkg/cmd/secret/list/list_test.go index bd14fafe9..6b1ffa253 100644 --- a/pkg/cmd/secret/list/list_test.go +++ b/pkg/cmd/secret/list/list_test.go @@ -54,6 +54,21 @@ func Test_NewCmdList(t *testing.T) { UserSecrets: true, }, }, + { + name: "Dependabot repo", + cli: "--app Dependabot", + wants: ListOptions{ + Application: "Dependabot", + }, + }, + { + name: "Dependabot org", + cli: "--app Dependabot --org UmbrellaCorporation", + wants: ListOptions{ + Application: "Dependabot", + OrgName: "UmbrellaCorporation", + }, + }, } for _, tt := range tests { @@ -184,6 +199,56 @@ func Test_listRun(t *testing.T) { "SECRET_THREE\t1975-11-30\t", }, }, + { + name: "Dependabot repo tty", + tty: true, + opts: &ListOptions{ + Application: "Dependabot", + }, + wantOut: []string{ + "SECRET_ONE.*Updated 1988-10-11", + "SECRET_TWO.*Updated 2020-12-04", + "SECRET_THREE.*Updated 1975-11-30", + }, + }, + { + name: "Dependabot repo not tty", + tty: false, + opts: &ListOptions{ + Application: "Dependabot", + }, + wantOut: []string{ + "SECRET_ONE\t1988-10-11", + "SECRET_TWO\t2020-12-04", + "SECRET_THREE\t1975-11-30", + }, + }, + { + name: "Dependabot org tty", + tty: true, + opts: &ListOptions{ + Application: "Dependabot", + OrgName: "UmbrellaCorporation", + }, + wantOut: []string{ + "SECRET_ONE.*Updated 1988-10-11.*Visible to all repositories", + "SECRET_TWO.*Updated 2020-12-04.*Visible to private repositories", + "SECRET_THREE.*Updated 1975-11-30.*Visible to 2 selected repositories", + }, + }, + { + name: "Dependabot org not tty", + tty: false, + opts: &ListOptions{ + Application: "Dependabot", + OrgName: "UmbrellaCorporation", + }, + wantOut: []string{ + "SECRET_ONE\t1988-10-11\tALL", + "SECRET_TWO\t2020-12-04\tPRIVATE", + "SECRET_THREE\t1975-11-30\tSELECTED", + }, + }, } for _, tt := range tests { @@ -280,6 +345,10 @@ func Test_listRun(t *testing.T) { } } + if tt.opts.Application == "Dependabot" { + path = strings.Replace(path, "actions", "dependabot", 1) + } + reg.Register(httpmock.REST("GET", path), httpmock.JSONResponse(payload)) io, _, stdout, _ := iostreams.Test() diff --git a/pkg/cmd/secret/remove/remove.go b/pkg/cmd/secret/remove/remove.go index 4ecf0b6cc..a4ec51277 100644 --- a/pkg/cmd/secret/remove/remove.go +++ b/pkg/cmd/secret/remove/remove.go @@ -8,6 +8,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/secret/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -23,6 +24,7 @@ type RemoveOptions struct { OrgName string EnvName string UserSecrets bool + Application string } func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Command { @@ -37,9 +39,9 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co Short: "Remove secrets", Long: heredoc.Doc(` Remove a secret on one of the following levels: - - repository (default): available to Actions runs in a repository + - repository (default): available to Actions runs or Dependabot in a repository - environment: available to Actions runs for a deployment environment in a repository - - organization: available to Actions runs within an organization + - organization: available to Actions runs or Dependabot within an organization - user: available to Codespaces for your user `), Args: cobra.ExactArgs(1), @@ -63,6 +65,7 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Remove a secret for an organization") cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Remove a secret for an environment") cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "Remove a secret for your user") + cmdutil.StringEnumFlag(cmd, &opts.Application, "app", "a", "", []string{shared.Actions, shared.Codespaces, shared.Dependabot}, "Remove a secret for a specific application") return cmd } @@ -77,8 +80,22 @@ func removeRun(opts *RemoveOptions) error { orgName := opts.OrgName envName := opts.EnvName + secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) + if err != nil { + return err + } + + secretApp, err := shared.GetSecretApp(opts.Application, secretEntity) + if err != nil { + return err + } + + if !shared.IsSupportedSecretEntity(secretApp, secretEntity) { + return fmt.Errorf("%s secrets are not supported for %s", secretEntity, secretApp) + } + var baseRepo ghrepo.Interface - if orgName == "" && !opts.UserSecrets { + if secretEntity == shared.Repository || secretEntity == shared.Environment { baseRepo, err = opts.BaseRepo() if err != nil { return fmt.Errorf("could not determine base repo: %w", err) @@ -86,14 +103,15 @@ func removeRun(opts *RemoveOptions) error { } var path string - if orgName != "" { - path = fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, opts.SecretName) - } else if envName != "" { + switch secretEntity { + case shared.Organization: + path = fmt.Sprintf("orgs/%s/%s/secrets/%s", orgName, secretApp, opts.SecretName) + case shared.Environment: path = fmt.Sprintf("repos/%s/environments/%s/secrets/%s", ghrepo.FullName(baseRepo), envName, opts.SecretName) - } else if opts.UserSecrets { + case shared.User: path = fmt.Sprintf("user/codespaces/secrets/%s", opts.SecretName) - } else { - path = fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(baseRepo), opts.SecretName) + case shared.Repository: + path = fmt.Sprintf("repos/%s/%s/secrets/%s", ghrepo.FullName(baseRepo), secretApp, opts.SecretName) } cfg, err := opts.Config() @@ -112,17 +130,21 @@ func removeRun(opts *RemoveOptions) error { } if opts.IO.IsStdoutTTY() { - target := orgName - if opts.UserSecrets { + var target string + switch secretEntity { + case shared.Organization: + target = orgName + case shared.User: target = "your user" - } else if orgName == "" { + case shared.Repository, shared.Environment: target = ghrepo.FullName(baseRepo) } + cs := opts.IO.ColorScheme() if envName != "" { fmt.Fprintf(opts.IO.Out, "%s Removed secret %s from %s environment on %s\n", cs.SuccessIconWithColor(cs.Red), opts.SecretName, envName, target) } else { - fmt.Fprintf(opts.IO.Out, "%s Removed secret %s from %s\n", cs.SuccessIconWithColor(cs.Red), opts.SecretName, target) + fmt.Fprintf(opts.IO.Out, "%s Removed %s secret %s from %s\n", cs.SuccessIconWithColor(cs.Red), secretApp.Title(), opts.SecretName, target) } } diff --git a/pkg/cmd/secret/remove/remove_test.go b/pkg/cmd/secret/remove/remove_test.go index 2abb38c64..f2b1a4d8e 100644 --- a/pkg/cmd/secret/remove/remove_test.go +++ b/pkg/cmd/secret/remove/remove_test.go @@ -2,7 +2,6 @@ package remove import ( "bytes" - "fmt" "net/http" "testing" @@ -57,6 +56,23 @@ func TestNewCmdRemove(t *testing.T) { UserSecrets: true, }, }, + { + name: "Dependabot repo", + cli: "cool --app Dependabot", + wants: RemoveOptions{ + SecretName: "cool", + Application: "Dependabot", + }, + }, + { + name: "Dependabot org", + cli: "cool --app Dependabot --org UmbrellaCorporation", + wants: RemoveOptions{ + SecretName: "cool", + OrgName: "UmbrellaCorporation", + Application: "Dependabot", + }, + }, } for _, tt := range tests { @@ -95,32 +111,61 @@ func TestNewCmdRemove(t *testing.T) { } func Test_removeRun_repo(t *testing.T) { - reg := &httpmock.Registry{} - - reg.Register( - httpmock.REST("DELETE", "repos/owner/repo/actions/secrets/cool_secret"), - httpmock.StatusStringResponse(204, "No Content")) - - io, _, _, _ := iostreams.Test() - - opts := &RemoveOptions{ - IO: io, - HttpClient: func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil + tests := []struct { + name string + opts *RemoveOptions + wantPath string + }{ + { + name: "Actions", + opts: &RemoveOptions{ + Application: "actions", + SecretName: "cool_secret", + }, + wantPath: "repos/owner/repo/actions/secrets/cool_secret", }, - Config: func() (config.Config, error) { - return config.NewBlankConfig(), nil + { + name: "Dependabot", + opts: &RemoveOptions{ + Application: "dependabot", + SecretName: "cool_dependabot_secret", + }, + wantPath: "repos/owner/repo/dependabot/secrets/cool_dependabot_secret", }, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("owner/repo") + { + name: "defaults to Actions", + opts: &RemoveOptions{ + SecretName: "cool_secret", + }, + wantPath: "repos/owner/repo/actions/secrets/cool_secret", }, - SecretName: "cool_secret", } - err := removeRun(opts) - assert.NoError(t, err) + for _, tt := range tests { + reg := &httpmock.Registry{} - reg.Verify(t) + reg.Register( + httpmock.REST("DELETE", tt.wantPath), + httpmock.StatusStringResponse(204, "No Content")) + + io, _, _, _ := iostreams.Test() + + tt.opts.IO = io + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + tt.opts.Config = func() (config.Config, error) { + return config.NewBlankConfig(), nil + } + tt.opts.BaseRepo = func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + } + + err := removeRun(tt.opts) + assert.NoError(t, err) + + reg.Verify(t) + } } func Test_removeRun_env(t *testing.T) { @@ -155,18 +200,24 @@ func Test_removeRun_env(t *testing.T) { func Test_removeRun_org(t *testing.T) { tests := []struct { - name string - opts *RemoveOptions + name string + opts *RemoveOptions + wantPath string }{ - { - name: "repo", - opts: &RemoveOptions{}, - }, { name: "org", opts: &RemoveOptions{ OrgName: "UmbrellaCorporation", }, + wantPath: "orgs/UmbrellaCorporation/actions/secrets/tVirus", + }, + { + name: "Dependabot org", + opts: &RemoveOptions{ + Application: "dependabot", + OrgName: "UmbrellaCorporation", + }, + wantPath: "orgs/UmbrellaCorporation/dependabot/secrets/tVirus", }, } @@ -174,17 +225,9 @@ func Test_removeRun_org(t *testing.T) { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} - orgName := tt.opts.OrgName - - if orgName == "" { - reg.Register( - httpmock.REST("DELETE", "repos/owner/repo/actions/secrets/tVirus"), - httpmock.StatusStringResponse(204, "No Content")) - } else { - reg.Register( - httpmock.REST("DELETE", fmt.Sprintf("orgs/%s/actions/secrets/tVirus", orgName)), - httpmock.StatusStringResponse(204, "No Content")) - } + reg.Register( + httpmock.REST("DELETE", tt.wantPath), + httpmock.StatusStringResponse(204, "No Content")) io, _, _, _ := iostreams.Test() From b48a93cddfb48738447c32a9d405dc4b062817d2 Mon Sep 17 00:00:00 2001 From: Steve Gray Date: Fri, 18 Mar 2022 14:06:27 -0600 Subject: [PATCH 70/75] Remove unwanted trailing quote Removes a stray quote from the codespace ssh example. --- pkg/cmd/codespace/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index dfbc75694..69c9c9ee1 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -59,7 +59,7 @@ func newSSHCmd(app *App) *cobra.Command { $ gh codespace ssh $ gh codespace ssh --config > ~/.ssh/codespaces - $ echo 'include ~/.ssh/codespaces' >> ~/.ssh/config' + $ echo 'include ~/.ssh/codespaces' >> ~/.ssh/config `), PreRunE: func(c *cobra.Command, args []string) error { if opts.stdio { From b090ef05789255f710147319abcf1a686f7e6cad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:27:30 +0000 Subject: [PATCH 71/75] Bump actions/cache from 2 to 3 Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/go.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4fbf3d3b9..e92122ece 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v3 - name: Restore Go modules cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/go/pkg/mod key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f22206f70..7e093c285 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v3 - name: Restore Go modules cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/go/pkg/mod key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} From 107ea6c440b4199f99d00a426a72c333dbc43f84 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 21 Mar 2022 11:09:52 -0400 Subject: [PATCH 72/75] Only sleep if conn is nil --- pkg/liveshare/port_forwarder_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index 1a4526f2a..deb17bcd0 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -85,7 +85,9 @@ func TestPortForwarderStart(t *testing.T) { retries := 0 for conn == nil && retries < 2 { conn, err = net.DialTimeout("tcp", ":8000", 2*time.Second) - time.Sleep(1 * time.Second) + if conn == nil { + time.Sleep(1 * time.Second) + } } if conn == nil { done <- errors.New("failed to connect to forwarded port") From 74c8b2217e70137ada72011c2010757a440c698c Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 21 Mar 2022 11:36:12 -0400 Subject: [PATCH 73/75] Increment retries --- pkg/liveshare/port_forwarder_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index deb17bcd0..a220486d6 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -86,6 +86,7 @@ func TestPortForwarderStart(t *testing.T) { for conn == nil && retries < 2 { conn, err = net.DialTimeout("tcp", ":8000", 2*time.Second) if conn == nil { + retries++ time.Sleep(1 * time.Second) } } From cd6176d91189a239e0860811722f1805905bd352 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 21 Mar 2022 11:42:32 -0400 Subject: [PATCH 74/75] Rename to tries and clean up loop condition --- pkg/liveshare/port_forwarder_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index a220486d6..990a14e36 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -82,11 +82,9 @@ func TestPortForwarderStart(t *testing.T) { go func() { var conn net.Conn - retries := 0 - for conn == nil && retries < 2 { + for tries := 0; conn == nil && tries < 2; tries++ { conn, err = net.DialTimeout("tcp", ":8000", 2*time.Second) if conn == nil { - retries++ time.Sleep(1 * time.Second) } } From b5a7a36ecedb9a92ebd629e14c2969349f99cfbd Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Mon, 21 Mar 2022 12:11:00 -0400 Subject: [PATCH 75/75] Document why we have a loop retrying --- pkg/liveshare/port_forwarder_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/liveshare/port_forwarder_test.go b/pkg/liveshare/port_forwarder_test.go index 990a14e36..93f8187d8 100644 --- a/pkg/liveshare/port_forwarder_test.go +++ b/pkg/liveshare/port_forwarder_test.go @@ -82,6 +82,8 @@ func TestPortForwarderStart(t *testing.T) { go func() { var conn net.Conn + + // We retry DialTimeout in a loop to deal with a race in PortForwarder startup. for tries := 0; conn == nil && tries < 2; tries++ { conn, err = net.DialTimeout("tcp", ":8000", 2*time.Second) if conn == nil {