Merge remote-tracking branch 'origin/trunk' into gh-ext-search

This commit is contained in:
vilmibm 2022-11-08 12:40:21 -08:00
commit 036e16608f
11 changed files with 1124 additions and 66 deletions

5
go.mod
View file

@ -15,6 +15,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.2
github.com/creack/pty v1.1.18
github.com/gabriel-vasile/mimetype v1.4.1
github.com/gdamore/tcell/v2 v2.5.3
github.com/google/go-cmp v0.5.9
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.4.2
@ -28,6 +29,7 @@ require (
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
github.com/opentracing/opentracing-go v1.1.0
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00
github.com/sourcegraph/jsonrpc2 v0.1.0
github.com/spf13/cobra v1.5.0
@ -50,6 +52,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
@ -63,7 +66,7 @@ require (
github.com/muesli/termenv v0.12.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
github.com/stretchr/objx v0.4.0 // indirect

11
go.sum
View file

@ -88,6 +88,10 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
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=
@ -218,9 +222,12 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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=
@ -370,12 +377,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/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-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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=

View file

@ -0,0 +1,16 @@
# Protocol Buffers for Codespaces
Instructions for generating and adding gRPC protocol buffers.
## Generate Protocol Buffers
1. [Download `protoc`](https://grpc.io/docs/protoc-installation/)
2. [Download protocol compiler plugins for Go](https://grpc.io/docs/languages/go/quickstart/)
3. Run `./generate.sh` from the `internal/codespaces/grpc` directory
## Add New Protocol Buffers
1. Download a `.proto` contract from the service repo
2. Create a new directory and copy the `.proto` to it
3. Update `generate.sh` to include the include the new `.proto`
4. Follow the instructions to [Generate Protocol Buffers](#generate-protocol-buffers)

View file

@ -0,0 +1,26 @@
#!/bin/bash
set -e
if ! protoc --version; then
echo 'ERROR: protoc is not on your PATH'
exit 1
fi
if ! protoc-gen-go --version; then
echo 'ERROR: protoc-gen-go is not on your PATH'
exit 1
fi
if ! protoc-gen-go-grpc --version; then
echo 'ERROR: protoc-gen-go-grpc is not on your PATH'
fi
function generate {
local contract="$1"
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative "$contract"
echo "Generated protocol buffers for $contract"
}
generate jupyter/JupyterServerHostService.v1.proto
echo 'Done!'

View file

@ -1,8 +1,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.21.3
// source: JupyterServerHostService.v1.proto
// protoc-gen-go v1.28.1
// protoc v3.12.4
// source: jupyter/JupyterServerHostService.v1.proto
package jupyter
@ -29,7 +29,7 @@ type GetRunningServerRequest struct {
func (x *GetRunningServerRequest) Reset() {
*x = GetRunningServerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_JupyterServerHostService_v1_proto_msgTypes[0]
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -42,7 +42,7 @@ func (x *GetRunningServerRequest) String() string {
func (*GetRunningServerRequest) ProtoMessage() {}
func (x *GetRunningServerRequest) ProtoReflect() protoreflect.Message {
mi := &file_JupyterServerHostService_v1_proto_msgTypes[0]
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -55,7 +55,7 @@ func (x *GetRunningServerRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetRunningServerRequest.ProtoReflect.Descriptor instead.
func (*GetRunningServerRequest) Descriptor() ([]byte, []int) {
return file_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{0}
return file_jupyter_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{0}
}
type GetRunningServerResponse struct {
@ -72,7 +72,7 @@ type GetRunningServerResponse struct {
func (x *GetRunningServerResponse) Reset() {
*x = GetRunningServerResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_JupyterServerHostService_v1_proto_msgTypes[1]
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -85,7 +85,7 @@ func (x *GetRunningServerResponse) String() string {
func (*GetRunningServerResponse) ProtoMessage() {}
func (x *GetRunningServerResponse) ProtoReflect() protoreflect.Message {
mi := &file_JupyterServerHostService_v1_proto_msgTypes[1]
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -98,7 +98,7 @@ func (x *GetRunningServerResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetRunningServerResponse.ProtoReflect.Descriptor instead.
func (*GetRunningServerResponse) Descriptor() ([]byte, []int) {
return file_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{1}
return file_jupyter_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{1}
}
func (x *GetRunningServerResponse) GetResult() bool {
@ -129,57 +129,57 @@ func (x *GetRunningServerResponse) GetServerUrl() string {
return ""
}
var File_JupyterServerHostService_v1_proto protoreflect.FileDescriptor
var File_jupyter_JupyterServerHostService_v1_proto protoreflect.FileDescriptor
var file_JupyterServerHostService_v1_proto_rawDesc = []byte{
0x0a, 0x21, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48,
0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x2b, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e,
0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31,
0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x7e, 0x0a, 0x18, 0x47,
0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c,
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12,
0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72,
0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a,
0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x32, 0xb5, 0x01, 0x0a, 0x11,
0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73,
0x74, 0x12, 0x9f, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x44, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61,
0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x45, 0x2e, 0x43,
0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a,
0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75,
0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x2f, 0x6a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
var file_jupyter_JupyterServerHostService_v1_proto_rawDesc = []byte{
0x0a, 0x29, 0x6a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x2f, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65,
0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2b, 0x43, 0x6f, 0x64,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70,
0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52,
0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x22, 0x7e, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e,
0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x16, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55,
0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x55, 0x72, 0x6c, 0x32, 0xb5, 0x01, 0x0a, 0x11, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x9f, 0x01, 0x0a, 0x10, 0x47, 0x65,
0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x44,
0x2e, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63,
0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f,
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x45, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e,
0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72,
0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2e,
0x2f, 0x6a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_JupyterServerHostService_v1_proto_rawDescOnce sync.Once
file_JupyterServerHostService_v1_proto_rawDescData = file_JupyterServerHostService_v1_proto_rawDesc
file_jupyter_JupyterServerHostService_v1_proto_rawDescOnce sync.Once
file_jupyter_JupyterServerHostService_v1_proto_rawDescData = file_jupyter_JupyterServerHostService_v1_proto_rawDesc
)
func file_JupyterServerHostService_v1_proto_rawDescGZIP() []byte {
file_JupyterServerHostService_v1_proto_rawDescOnce.Do(func() {
file_JupyterServerHostService_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_JupyterServerHostService_v1_proto_rawDescData)
func file_jupyter_JupyterServerHostService_v1_proto_rawDescGZIP() []byte {
file_jupyter_JupyterServerHostService_v1_proto_rawDescOnce.Do(func() {
file_jupyter_JupyterServerHostService_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_jupyter_JupyterServerHostService_v1_proto_rawDescData)
})
return file_JupyterServerHostService_v1_proto_rawDescData
return file_jupyter_JupyterServerHostService_v1_proto_rawDescData
}
var file_JupyterServerHostService_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_JupyterServerHostService_v1_proto_goTypes = []interface{}{
var file_jupyter_JupyterServerHostService_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_jupyter_JupyterServerHostService_v1_proto_goTypes = []interface{}{
(*GetRunningServerRequest)(nil), // 0: Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerRequest
(*GetRunningServerResponse)(nil), // 1: Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerResponse
}
var file_JupyterServerHostService_v1_proto_depIdxs = []int32{
var file_jupyter_JupyterServerHostService_v1_proto_depIdxs = []int32{
0, // 0: Codespaces.Grpc.JupyterServerHostService.v1.JupyterServerHost.GetRunningServer:input_type -> Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerRequest
1, // 1: Codespaces.Grpc.JupyterServerHostService.v1.JupyterServerHost.GetRunningServer:output_type -> Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerResponse
1, // [1:2] is the sub-list for method output_type
@ -189,13 +189,13 @@ var file_JupyterServerHostService_v1_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for field type_name
}
func init() { file_JupyterServerHostService_v1_proto_init() }
func file_JupyterServerHostService_v1_proto_init() {
if File_JupyterServerHostService_v1_proto != nil {
func init() { file_jupyter_JupyterServerHostService_v1_proto_init() }
func file_jupyter_JupyterServerHostService_v1_proto_init() {
if File_jupyter_JupyterServerHostService_v1_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_JupyterServerHostService_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
file_jupyter_JupyterServerHostService_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetRunningServerRequest); i {
case 0:
return &v.state
@ -207,7 +207,7 @@ func file_JupyterServerHostService_v1_proto_init() {
return nil
}
}
file_JupyterServerHostService_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
file_jupyter_JupyterServerHostService_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetRunningServerResponse); i {
case 0:
return &v.state
@ -224,18 +224,18 @@ func file_JupyterServerHostService_v1_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_JupyterServerHostService_v1_proto_rawDesc,
RawDescriptor: file_jupyter_JupyterServerHostService_v1_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_JupyterServerHostService_v1_proto_goTypes,
DependencyIndexes: file_JupyterServerHostService_v1_proto_depIdxs,
MessageInfos: file_JupyterServerHostService_v1_proto_msgTypes,
GoTypes: file_jupyter_JupyterServerHostService_v1_proto_goTypes,
DependencyIndexes: file_jupyter_JupyterServerHostService_v1_proto_depIdxs,
MessageInfos: file_jupyter_JupyterServerHostService_v1_proto_msgTypes,
}.Build()
File_JupyterServerHostService_v1_proto = out.File
file_JupyterServerHostService_v1_proto_rawDesc = nil
file_JupyterServerHostService_v1_proto_goTypes = nil
file_JupyterServerHostService_v1_proto_depIdxs = nil
File_jupyter_JupyterServerHostService_v1_proto = out.File
file_jupyter_JupyterServerHostService_v1_proto_rawDesc = nil
file_jupyter_JupyterServerHostService_v1_proto_goTypes = nil
file_jupyter_JupyterServerHostService_v1_proto_depIdxs = nil
}

View file

@ -1,8 +1,8 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.3
// source: JupyterServerHostService.v1.proto
// - protoc v3.12.4
// source: jupyter/JupyterServerHostService.v1.proto
package jupyter
@ -101,5 +101,5 @@ var JupyterServerHost_ServiceDesc = grpc.ServiceDesc{
},
},
Streams: []grpc.StreamDesc{},
Metadata: "JupyterServerHostService.v1.proto",
Metadata: "jupyter/JupyterServerHostService.v1.proto",
}

View file

@ -0,0 +1,527 @@
package browse
import (
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/charmbracelet/glamour"
"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/extensions"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/search"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/spf13/cobra"
)
const pagingOffset = 24
type ExtBrowseOpts struct {
Cmd *cobra.Command
Browser ibrowser
IO *iostreams.IOStreams
Searcher search.Searcher
Em extensions.ExtensionManager
Client *http.Client
Logger *log.Logger
Cfg config.Config
Rg *readmeGetter
Debug bool
}
type ibrowser interface {
Browse(string) error
}
type uiRegistry struct {
// references to some of the heavily cross-referenced tview primitives. Not
// everything is in here because most things are just used once in one place
// and don't need to be easy to look up like this.
App *tview.Application
Outerflex *tview.Flex
List *tview.List
Readme *tview.TextView
}
type extEntry struct {
URL string
Name string
FullName string
Installed bool
Official bool
description string
}
func (e extEntry) Title() string {
var installed string
var official string
if e.Installed {
installed = " [green](installed)"
}
if e.Official {
official = " [yellow](official)"
}
return fmt.Sprintf("%s%s%s", e.FullName, official, installed)
}
func (e extEntry) Description() string {
if e.description == "" {
return "no description provided"
}
return e.description
}
type extList struct {
ui uiRegistry
extEntries []extEntry
app *tview.Application
filter string
opts ExtBrowseOpts
}
func newExtList(opts ExtBrowseOpts, ui uiRegistry, extEntries []extEntry) *extList {
ui.List.SetTitleColor(tcell.ColorWhite)
ui.List.SetSelectedTextColor(tcell.ColorBlack)
ui.List.SetSelectedBackgroundColor(tcell.ColorWhite)
ui.List.SetWrapAround(false)
ui.List.SetBorderPadding(1, 1, 1, 1)
el := &extList{
ui: ui,
extEntries: extEntries,
app: ui.App,
opts: opts,
}
el.Reset()
return el
}
func (el *extList) createModal() *tview.Modal {
m := tview.NewModal()
m.SetBackgroundColor(tcell.ColorPurple)
m.SetDoneFunc(func(_ int, _ string) {
el.ui.App.SetRoot(el.ui.Outerflex, true)
el.Refresh()
})
return m
}
func (el *extList) InstallSelected() {
ee, ix := el.FindSelected()
if ix < 0 {
el.opts.Logger.Println("failed to find selected entry")
return
}
repo, err := ghrepo.FromFullName(ee.FullName)
if err != nil {
el.opts.Logger.Println(fmt.Errorf("failed to install '%s't: %w", ee.FullName, err))
return
}
modal := el.createModal()
modal.SetText(fmt.Sprintf("Installing %s...", ee.FullName))
el.ui.App.SetRoot(modal, true)
// I could eliminate this with a goroutine but it seems to be working fine
el.app.ForceDraw()
err = el.opts.Em.Install(repo, "")
if err != nil {
modal.SetText(fmt.Sprintf("Failed to install %s: %s", ee.FullName, err.Error()))
} else {
modal.SetText(fmt.Sprintf("Installed %s!", ee.FullName))
modal.AddButtons([]string{"ok"})
el.ui.App.SetFocus(modal)
}
el.toggleInstalled(ix)
}
func (el *extList) RemoveSelected() {
ee, ix := el.FindSelected()
if ix < 0 {
el.opts.Logger.Println("failed to find selected extension")
return
}
modal := el.createModal()
modal.SetText(fmt.Sprintf("Removing %s...", ee.FullName))
el.ui.App.SetRoot(modal, true)
// I could eliminate this with a goroutine but it seems to be working fine
el.ui.App.ForceDraw()
err := el.opts.Em.Remove(strings.TrimPrefix(ee.Name, "gh-"))
if err != nil {
modal.SetText(fmt.Sprintf("Failed to remove %s: %s", ee.FullName, err.Error()))
} else {
modal.SetText(fmt.Sprintf("Removed %s.", ee.FullName))
modal.AddButtons([]string{"ok"})
el.ui.App.SetFocus(modal)
}
el.toggleInstalled(ix)
}
func (el *extList) toggleInstalled(ix int) {
ee := el.extEntries[ix]
ee.Installed = !ee.Installed
el.extEntries[ix] = ee
}
func (el *extList) Focus() {
el.app.SetFocus(el.ui.List)
}
func (el *extList) Refresh() {
el.Reset()
el.Filter(el.filter)
}
func (el *extList) Reset() {
el.ui.List.Clear()
for _, ee := range el.extEntries {
el.ui.List.AddItem(ee.Title(), ee.Description(), rune(0), func() {})
}
}
func (el *extList) PageDown() {
el.ui.List.SetCurrentItem(el.ui.List.GetCurrentItem() + pagingOffset)
}
func (el *extList) PageUp() {
i := el.ui.List.GetCurrentItem() - pagingOffset
if i < 0 {
i = 0
}
el.ui.List.SetCurrentItem(i)
}
func (el *extList) ScrollDown() {
el.ui.List.SetCurrentItem(el.ui.List.GetCurrentItem() + 1)
}
func (el *extList) ScrollUp() {
i := el.ui.List.GetCurrentItem() - 1
if i < 0 {
i = 0
}
el.ui.List.SetCurrentItem(i)
}
func (el *extList) FindSelected() (extEntry, int) {
if el.ui.List.GetItemCount() == 0 {
return extEntry{}, -1
}
title, desc := el.ui.List.GetItemText(el.ui.List.GetCurrentItem())
for x, e := range el.extEntries {
if e.Title() == title && e.Description() == desc {
return e, x
}
}
return extEntry{}, -1
}
func (el *extList) Filter(text string) {
el.filter = text
if text == "" {
return
}
el.ui.List.Clear()
for _, ee := range el.extEntries {
if strings.Contains(ee.Title()+ee.Description(), text) {
el.ui.List.AddItem(ee.Title(), ee.Description(), rune(0), func() {})
}
}
}
func getSelectedReadme(opts ExtBrowseOpts, readme *tview.TextView, el *extList) (string, error) {
ee, ix := el.FindSelected()
if ix < 0 {
return "", errors.New("failed to find selected entry")
}
fullName := ee.FullName
rm, err := opts.Rg.Get(fullName)
if err != nil {
return "", err
}
_, _, wrap, _ := readme.GetInnerRect()
// using glamour directly because if I don't horrible things happen
renderer, err := glamour.NewTermRenderer(
glamour.WithStylePath("dark"),
glamour.WithWordWrap(wrap))
if err != nil {
return "", err
}
rendered, err := renderer.Render(rm)
if err != nil {
return "", err
}
return rendered, nil
}
func getExtensions(opts ExtBrowseOpts) ([]extEntry, error) {
extEntries := []extEntry{}
installed := opts.Em.List()
result, err := opts.Searcher.Repositories(search.Query{
Kind: search.KindRepositories,
Limit: 1000,
Qualifiers: search.Qualifiers{
Topic: []string{"gh-extension"},
},
})
if err != nil {
return extEntries, fmt.Errorf("failed to search for extensions: %w", err)
}
host, _ := opts.Cfg.DefaultHost()
for _, repo := range result.Items {
if !strings.HasPrefix(repo.Name, "gh-") {
continue
}
ee := extEntry{
URL: "https://" + host + "/" + repo.FullName,
FullName: repo.FullName,
Name: repo.Name,
description: repo.Description,
}
for _, v := range installed {
// TODO consider a Repo() on Extension interface
var installedRepo string
if u, err := git.ParseURL(v.URL()); err == nil {
if r, err := ghrepo.FromURL(u); err == nil {
installedRepo = ghrepo.FullName(r)
}
}
if repo.FullName == installedRepo {
ee.Installed = true
}
}
if repo.Owner.Login == "cli" || repo.Owner.Login == "github" {
ee.Official = true
}
extEntries = append(extEntries, ee)
}
return extEntries, nil
}
func ExtBrowse(opts ExtBrowseOpts) error {
if opts.Debug {
f, err := os.CreateTemp("", "extBrowse-*.txt")
if err != nil {
return err
}
defer os.Remove(f.Name())
opts.Logger = log.New(f, "", log.Lshortfile)
} else {
opts.Logger = log.New(io.Discard, "", 0)
}
opts.IO.StartProgressIndicator()
extEntries, err := getExtensions(opts)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
opts.Rg = newReadmeGetter(opts.Client, time.Hour*24)
app := tview.NewApplication()
outerFlex := tview.NewFlex()
innerFlex := tview.NewFlex()
header := tview.NewTextView().SetText(fmt.Sprintf("browsing %d gh extensions", len(extEntries)))
header.SetTextAlign(tview.AlignCenter).SetTextColor(tcell.ColorWhite)
filter := tview.NewInputField().SetLabel("filter: ")
filter.SetFieldBackgroundColor(tcell.ColorGray)
filter.SetBorderPadding(0, 0, 20, 20)
list := tview.NewList()
readme := tview.NewTextView()
readme.SetBorderPadding(1, 1, 0, 1)
readme.SetBorder(true).SetBorderColor(tcell.ColorPurple)
help := tview.NewTextView()
help.SetText(
"/: filter i/r: install/remove w: open in browser pgup/pgdn: scroll readme q: quit")
help.SetTextAlign(tview.AlignCenter)
ui := uiRegistry{
App: app,
Outerflex: outerFlex,
List: list,
}
extList := newExtList(opts, ui, extEntries)
loadSelectedReadme := func() {
rendered, err := getSelectedReadme(opts, readme, extList)
if err != nil {
opts.Logger.Println(err.Error())
readme.SetText("unable to fetch readme :(")
return
}
app.QueueUpdateDraw(func() {
readme.SetText("")
readme.SetDynamicColors(true)
w := tview.ANSIWriter(readme)
_, _ = w.Write([]byte(rendered))
readme.ScrollToBeginning()
})
}
filter.SetChangedFunc(func(text string) {
extList.Filter(text)
go loadSelectedReadme()
})
filter.SetDoneFunc(func(key tcell.Key) {
switch key {
case tcell.KeyEnter:
extList.Focus()
case tcell.KeyEscape:
filter.SetText("")
extList.Reset()
extList.Focus()
}
})
innerFlex.SetDirection(tview.FlexColumn)
innerFlex.AddItem(list, 0, 1, true)
innerFlex.AddItem(readme, 0, 1, false)
outerFlex.SetDirection(tview.FlexRow)
outerFlex.AddItem(header, 1, -1, false)
outerFlex.AddItem(filter, 1, -1, false)
outerFlex.AddItem(innerFlex, 0, 1, true)
outerFlex.AddItem(help, 1, -1, false)
app.SetRoot(outerFlex, true)
// Force fetching of initial readme by loading it just prior to the first
// draw. The callback is removed immediately after draw.
app.SetBeforeDrawFunc(func(_ tcell.Screen) bool {
go loadSelectedReadme()
return false // returning true would halt drawing which we do not want
})
app.SetAfterDrawFunc(func(_ tcell.Screen) {
app.SetBeforeDrawFunc(nil)
app.SetAfterDrawFunc(nil)
})
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if filter.HasFocus() {
return event
}
switch event.Rune() {
case 'q':
app.Stop()
case 'k':
extList.ScrollUp()
readme.SetText("...fetching readme...")
go loadSelectedReadme()
case 'j':
extList.ScrollDown()
readme.SetText("...fetching readme...")
go loadSelectedReadme()
case 'w':
ee, ix := extList.FindSelected()
if ix < 0 {
opts.Logger.Println("failed to find selected entry")
return nil
}
err = opts.Browser.Browse(ee.URL)
if err != nil {
opts.Logger.Println(fmt.Errorf("could not open browser for '%s': %w", ee.URL, err))
}
case 'i':
extList.InstallSelected()
case 'r':
extList.RemoveSelected()
case ' ':
// The shift check works on windows and not linux/mac:
if event.Modifiers()&tcell.ModShift != 0 {
extList.PageUp()
} else {
extList.PageDown()
}
go loadSelectedReadme()
case '/':
app.SetFocus(filter)
return nil
}
switch event.Key() {
case tcell.KeyUp:
extList.ScrollUp()
go loadSelectedReadme()
return nil
case tcell.KeyDown:
extList.ScrollDown()
go loadSelectedReadme()
return nil
case tcell.KeyEscape:
filter.SetText("")
extList.Reset()
case tcell.KeyCtrlSpace:
// The ctrl check works on windows/mac and not windows:
extList.PageUp()
go loadSelectedReadme()
case tcell.KeyCtrlJ:
extList.PageDown()
go loadSelectedReadme()
case tcell.KeyCtrlK:
extList.PageUp()
go loadSelectedReadme()
case tcell.KeyPgUp:
row, col := readme.GetScrollOffset()
if row > 0 {
readme.ScrollTo(row-2, col)
}
return nil
case tcell.KeyPgDn:
row, col := readme.GetScrollOffset()
readme.ScrollTo(row+2, col)
return nil
}
return event
})
// Without this redirection, the git client inside of the extension manager
// will dump git output to the terminal.
opts.IO.ErrOut = io.Discard
if err := app.Run(); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,366 @@
package browse
import (
"encoding/base64"
"io"
"log"
"net/http"
"net/url"
"testing"
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/view"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/search"
"github.com/rivo/tview"
"github.com/stretchr/testify/assert"
)
func Test_getSelectedReadme(t *testing.T) {
reg := httpmock.Registry{}
defer reg.Verify(t)
content := base64.StdEncoding.EncodeToString([]byte("lol"))
reg.Register(
httpmock.REST("GET", "repos/cli/gh-cool/readme"),
httpmock.JSONResponse(view.RepoReadme{Content: content}))
client := &http.Client{Transport: &reg}
rg := newReadmeGetter(client, time.Second)
opts := ExtBrowseOpts{
Rg: rg,
}
readme := tview.NewTextView()
ui := uiRegistry{
List: tview.NewList(),
}
extEntries := []extEntry{
{
Name: "gh-cool",
FullName: "cli/gh-cool",
Installed: false,
Official: true,
description: "it's just cool ok",
},
{
Name: "gh-screensaver",
FullName: "vilmibm/gh-screensaver",
Installed: true,
Official: false,
description: "animations in your terminal",
},
}
el := newExtList(opts, ui, extEntries)
content, err := getSelectedReadme(opts, readme, el)
assert.NoError(t, err)
assert.Contains(t, content, "lol")
}
func Test_getExtensionRepos(t *testing.T) {
reg := httpmock.Registry{}
defer reg.Verify(t)
client := &http.Client{Transport: &reg}
values := url.Values{
"page": []string{"1"},
"per_page": []string{"100"},
"q": []string{"topic:gh-extension"},
}
cfg := config.NewBlankConfig()
cfg.DefaultHostFunc = func() (string, string) { return "github.com", "" }
reg.Register(
httpmock.QueryMatcher("GET", "search/repositories", values),
httpmock.JSONResponse(search.RepositoriesResult{
IncompleteResults: false,
Items: []search.Repository{
{
FullName: "vilmibm/gh-screensaver",
Name: "gh-screensaver",
Description: "terminal animations",
Owner: search.User{
Login: "vilmibm",
},
},
{
FullName: "cli/gh-cool",
Name: "gh-cool",
Description: "it's just cool ok",
Owner: search.User{
Login: "cli",
},
},
{
FullName: "samcoe/gh-triage",
Name: "gh-triage",
Description: "helps with triage",
Owner: search.User{
Login: "samcoe",
},
},
{
FullName: "github/gh-gei",
Name: "gh-gei",
Description: "something something enterprise",
Owner: search.User{
Login: "github",
},
},
},
Total: 4,
}),
)
searcher := search.NewSearcher(client, "github.com")
emMock := &extensions.ExtensionManagerMock{}
emMock.ListFunc = func() []extensions.Extension {
return []extensions.Extension{
&extensions.ExtensionMock{
URLFunc: func() string {
return "https://github.com/vilmibm/gh-screensaver"
},
},
&extensions.ExtensionMock{
URLFunc: func() string {
return "https://github.com/github/gh-gei"
},
},
}
}
opts := ExtBrowseOpts{
Searcher: searcher,
Em: emMock,
Cfg: cfg,
}
extEntries, err := getExtensions(opts)
assert.NoError(t, err)
expectedEntries := []extEntry{
{
URL: "https://github.com/vilmibm/gh-screensaver",
Name: "gh-screensaver",
FullName: "vilmibm/gh-screensaver",
Installed: true,
Official: false,
description: "terminal animations",
},
{
URL: "https://github.com/cli/gh-cool",
Name: "gh-cool",
FullName: "cli/gh-cool",
Installed: false,
Official: true,
description: "it's just cool ok",
},
{
URL: "https://github.com/samcoe/gh-triage",
Name: "gh-triage",
FullName: "samcoe/gh-triage",
Installed: false,
Official: false,
description: "helps with triage",
},
{
URL: "https://github.com/github/gh-gei",
Name: "gh-gei",
FullName: "github/gh-gei",
Installed: true,
Official: true,
description: "something something enterprise",
},
}
assert.Equal(t, expectedEntries, extEntries)
}
func Test_extEntry(t *testing.T) {
cases := []struct {
name string
ee extEntry
expectedTitle string
expectedDesc string
}{
{
name: "official",
ee: extEntry{
Name: "gh-cool",
FullName: "cli/gh-cool",
Installed: false,
Official: true,
description: "it's just cool ok",
},
expectedTitle: "cli/gh-cool [yellow](official)",
expectedDesc: "it's just cool ok",
},
{
name: "no description",
ee: extEntry{
Name: "gh-nodesc",
FullName: "barryburton/gh-nodesc",
Installed: false,
Official: false,
description: "",
},
expectedTitle: "barryburton/gh-nodesc",
expectedDesc: "no description provided",
},
{
name: "installed",
ee: extEntry{
Name: "gh-screensaver",
FullName: "vilmibm/gh-screensaver",
Installed: true,
Official: false,
description: "animations in your terminal",
},
expectedTitle: "vilmibm/gh-screensaver [green](installed)",
expectedDesc: "animations in your terminal",
},
{
name: "neither",
ee: extEntry{
Name: "gh-triage",
FullName: "samcoe/gh-triage",
Installed: false,
Official: false,
description: "help with triage",
},
expectedTitle: "samcoe/gh-triage",
expectedDesc: "help with triage",
},
{
name: "both",
ee: extEntry{
Name: "gh-gei",
FullName: "github/gh-gei",
Installed: true,
Official: true,
description: "something something enterprise",
},
expectedTitle: "github/gh-gei [yellow](official) [green](installed)",
expectedDesc: "something something enterprise",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expectedTitle, tt.ee.Title())
assert.Equal(t, tt.expectedDesc, tt.ee.Description())
})
}
}
func Test_extList(t *testing.T) {
opts := ExtBrowseOpts{
Logger: log.New(io.Discard, "", 0),
Em: &extensions.ExtensionManagerMock{
InstallFunc: func(repo ghrepo.Interface, _ string) error {
assert.Equal(t, "cli/gh-cool", ghrepo.FullName(repo))
return nil
},
RemoveFunc: func(name string) error {
assert.Equal(t, "cool", name)
return nil
},
},
}
app := tview.NewApplication()
list := tview.NewList()
ui := uiRegistry{
List: list,
App: app,
}
extEntries := []extEntry{
{
Name: "gh-cool",
FullName: "cli/gh-cool",
Installed: false,
Official: true,
description: "it's just cool ok",
},
{
Name: "gh-screensaver",
FullName: "vilmibm/gh-screensaver",
Installed: true,
Official: false,
description: "animations in your terminal",
},
{
Name: "gh-triage",
FullName: "samcoe/gh-triage",
Installed: false,
Official: false,
description: "help with triage",
},
{
Name: "gh-gei",
FullName: "github/gh-gei",
Installed: true,
Official: true,
description: "something something enterprise",
},
}
extList := newExtList(opts, ui, extEntries)
extList.Filter("cool")
assert.Equal(t, 1, extList.ui.List.GetItemCount())
title, _ := extList.ui.List.GetItemText(0)
assert.Equal(t, "cli/gh-cool [yellow](official)", title)
extList.InstallSelected()
assert.True(t, extList.extEntries[0].Installed)
extList.Refresh()
assert.Equal(t, 1, extList.ui.List.GetItemCount())
title, _ = extList.ui.List.GetItemText(0)
assert.Equal(t, "cli/gh-cool [yellow](official) [green](installed)", title)
extList.RemoveSelected()
assert.False(t, extList.extEntries[0].Installed)
extList.Refresh()
assert.Equal(t, 1, extList.ui.List.GetItemCount())
title, _ = extList.ui.List.GetItemText(0)
assert.Equal(t, "cli/gh-cool [yellow](official)", title)
extList.Reset()
assert.Equal(t, 4, extList.ui.List.GetItemCount())
ee, ix := extList.FindSelected()
assert.Equal(t, 0, ix)
assert.Equal(t, "cli/gh-cool [yellow](official)", ee.Title())
extList.ScrollDown()
ee, ix = extList.FindSelected()
assert.Equal(t, 1, ix)
assert.Equal(t, "vilmibm/gh-screensaver [green](installed)", ee.Title())
extList.ScrollUp()
ee, ix = extList.FindSelected()
assert.Equal(t, 0, ix)
assert.Equal(t, "cli/gh-cool [yellow](official)", ee.Title())
extList.PageDown()
ee, ix = extList.FindSelected()
assert.Equal(t, 3, ix)
assert.Equal(t, "github/gh-gei [yellow](official) [green](installed)", ee.Title())
extList.PageUp()
ee, ix = extList.FindSelected()
assert.Equal(t, 0, ix)
assert.Equal(t, "cli/gh-cool [yellow](official)", ee.Title())
}

View file

@ -0,0 +1,33 @@
package browse
import (
"net/http"
"time"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/view"
)
type readmeGetter struct {
client *http.Client
}
func newReadmeGetter(client *http.Client, cacheTTL time.Duration) *readmeGetter {
cachingClient := api.NewCachedHTTPClient(client, cacheTTL)
return &readmeGetter{
client: cachingClient,
}
}
func (g *readmeGetter) Get(repoFullName string) (string, error) {
repo, err := ghrepo.FromFullName(repoFullName)
if err != nil {
return "", err
}
readme, err := view.RepositoryReadme(g.client, repo, "")
if err != nil {
return "", err
}
return readme.Content, nil
}

View file

@ -5,12 +5,15 @@ import (
"fmt"
"os"
"strings"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/extension/browse"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/search"
@ -239,6 +242,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
t := utils.NewTablePrinter(io)
for _, c := range cmds {
// TODO consider a Repo() on Extension interface
var repo string
if u, err := git.ParseURL(c.URL()); err == nil {
if r, err := ghrepo.FromURL(u); err == nil {
@ -406,6 +410,74 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
return nil
},
},
func() *cobra.Command {
var debug bool
cmd := &cobra.Command{
Use: "browse",
Short: "Enter a UI for browsing, adding, and removing extensions",
Long: heredoc.Doc(`
This command will take over your terminal and run a fully interactive
interface for browsing, adding, and removing gh extensions.
The extension list is navigated with the arrow keys or with j/k.
Space and control+space (or control + j/k) page the list up and down.
Extension readmes can be scrolled with page up/page down keys
(fn + arrow up/down on a mac keyboard).
For highlighted extensions, you can press:
- w to open the extension in your web browser
- i to install the extension
- r to remove the extension
Press / to focus the filter input. Press enter to scroll the results.
Press Escape to clear the filter and return to the full list.
Press q to quit.
The output of this command may be difficult to navigate for screen reader
users, users operating at high zoom and other users of assistive technology. It
is also not advised for automation scripts. We advise those users to use the
alternative command:
gh ext search
along with gh ext install, gh ext remove, and gh repo view.
`),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if !io.CanPrompt() {
return errors.New("this command runs an interactive UI and needs to be run in a terminal")
}
cfg, err := config()
if err != nil {
return err
}
host, _ := cfg.DefaultHost()
client, err := f.HttpClient()
if err != nil {
return err
}
searcher := search.NewSearcher(api.NewCachedHTTPClient(client, time.Hour*24), host)
opts := browse.ExtBrowseOpts{
Cmd: cmd,
IO: io,
Browser: browser,
Searcher: searcher,
Em: m,
Client: client,
Cfg: cfg,
Debug: debug,
}
return browse.ExtBrowse(opts)
},
}
cmd.Flags().BoolVar(&debug, "debug", false, "log to /tmp/extBrowse-*")
return cmd
}(),
&cobra.Command{
Use: "exec <name> [args]",
Short: "Execute an installed extension",

View file

@ -756,6 +756,12 @@ func TestNewCmdExtension(t *testing.T) {
},
wantStdout: "test output",
},
{
name: "browse",
args: []string{"browse"},
wantErr: true,
errMsg: "this command runs an interactive UI and needs to be run in a terminal",
},
}
for _, tt := range tests {