Merge branch 'trunk' into repo-rename
This commit is contained in:
commit
c83c6a83e6
83 changed files with 624 additions and 583 deletions
|
|
@ -5,7 +5,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
|
||||
func (issue *Issue) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(issue).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
|
|
@ -25,10 +25,10 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
return data
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
|
||||
func (pr *PullRequest) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(pr).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
return data
|
||||
}
|
||||
|
||||
func fieldByName(v reflect.Value, field string) reflect.Value {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"reflect"
|
||||
)
|
||||
|
||||
func (repo *Repository) ExportData(fields []string) *map[string]interface{} {
|
||||
func (repo *Repository) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(repo).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ func (repo *Repository) ExportData(fields []string) *map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
return data
|
||||
}
|
||||
|
||||
func miniRepoExport(r *Repository) map[string]interface{} {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ check your internet connection or https://githubstatus.com
|
|||
{
|
||||
name: "Cobra flag error",
|
||||
args: args{
|
||||
err: &cmdutil.FlagError{Err: errors.New("unknown flag --foo")},
|
||||
err: cmdutil.FlagErrorf("unknown flag --foo"),
|
||||
cmd: cmd,
|
||||
debug: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,14 +60,14 @@ and talk through which code gets run in order.
|
|||
|
||||
## How to add a new command
|
||||
|
||||
0. First, check on our issue tracker to verify that our team had approved the plans for a new command.
|
||||
1. Create a package for the new command, e.g. for a new command `gh boom` create the following directory
|
||||
1. First, check on our issue tracker to verify that our team had approved the plans for a new command.
|
||||
2. Create a package for the new command, e.g. for a new command `gh boom` create the following directory
|
||||
structure: `pkg/cmd/boom/`
|
||||
2. The new package should expose a method, e.g. `NewCmdBoom()`, that accepts a `*cmdutil.Factory` type and
|
||||
3. The new package should expose a method, e.g. `NewCmdBoom()`, that accepts a `*cmdutil.Factory` type and
|
||||
returns a `*cobra.Command`.
|
||||
* Any logic specific to this command should be kept within the command's package and not added to any
|
||||
"global" packages like `api` or `utils`.
|
||||
3. Use the method from the previous step to generate the command and add it to the command tree, typically
|
||||
4. Use the method from the previous step to generate the command and add it to the command tree, typically
|
||||
somewhere in the `NewCmdRoot()` method.
|
||||
|
||||
## How to write tests
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ If the build fails, there is not a clean way to re-run it. The easiest way would
|
|||
|
||||
A local release can be created for testing without creating anything official on the release page.
|
||||
|
||||
0. Make sure GoReleaser is installed: `brew install goreleaser`
|
||||
1. `goreleaser --skip-validate --skip-publish --rm-dist`
|
||||
2. Find the built products under `dist/`.
|
||||
1. Make sure GoReleaser is installed: `brew install goreleaser`
|
||||
2. `goreleaser --skip-validate --skip-publish --rm-dist`
|
||||
3. Find the built products under `dist/`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Installation from source
|
||||
|
||||
0. Verify that you have Go 1.16+ installed
|
||||
1. Verify that you have Go 1.16+ installed
|
||||
|
||||
```sh
|
||||
$ go version
|
||||
|
|
@ -8,14 +8,14 @@
|
|||
|
||||
If `go` is not installed, follow instructions on [the Go website](https://golang.org/doc/install).
|
||||
|
||||
1. Clone this repository
|
||||
2. Clone this repository
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/cli/cli.git gh-cli
|
||||
$ cd gh-cli
|
||||
```
|
||||
|
||||
2. Build and install
|
||||
3. Build and install
|
||||
|
||||
#### Unix-like systems
|
||||
```sh
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
```
|
||||
There is no install step available on Windows.
|
||||
|
||||
3. Run `gh version` to check if it worked.
|
||||
4. Run `gh version` to check if it worked.
|
||||
|
||||
#### Windows
|
||||
Run `bin\gh version` to check if it worked.
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -23,10 +23,10 @@ require (
|
|||
github.com/mattn/go-colorable v0.1.11
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/microcosm-cc/bluemonday v1.0.16 // indirect
|
||||
github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5
|
||||
github.com/muesli/termenv v0.9.0
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -263,8 +263,9 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=
|
||||
github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
|
|
@ -432,8 +433,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -104,8 +105,9 @@ func (a *API) GetUser(ctx context.Context) (*User, error) {
|
|||
|
||||
// Repository represents a GitHub repository.
|
||||
type Repository struct {
|
||||
ID int `json:"id"`
|
||||
FullName string `json:"full_name"`
|
||||
ID int `json:"id"`
|
||||
FullName string `json:"full_name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
|
||||
// GetRepository returns the repository associated with the given owner and name.
|
||||
|
|
@ -162,6 +164,10 @@ type CodespaceGitStatus struct {
|
|||
const (
|
||||
// CodespaceStateAvailable is the state for a running codespace environment.
|
||||
CodespaceStateAvailable = "Available"
|
||||
// CodespaceStateShutdown is the state for a shutdown codespace environment.
|
||||
CodespaceStateShutdown = "Shutdown"
|
||||
// CodespaceStateStarting is the state for a starting codespace environment.
|
||||
CodespaceStateStarting = "Starting"
|
||||
)
|
||||
|
||||
type CodespaceConnection struct {
|
||||
|
|
@ -172,6 +178,44 @@ type CodespaceConnection struct {
|
|||
HostPublicKeys []string `json:"hostPublicKeys"`
|
||||
}
|
||||
|
||||
// CodespaceFields is the list of exportable fields for a codespace.
|
||||
var CodespaceFields = []string{
|
||||
"name",
|
||||
"owner",
|
||||
"repository",
|
||||
"state",
|
||||
"gitStatus",
|
||||
"createdAt",
|
||||
"lastUsedAt",
|
||||
}
|
||||
|
||||
func (c *Codespace) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "owner":
|
||||
data[f] = c.Owner.Login
|
||||
case "repository":
|
||||
data[f] = c.Repository.FullName
|
||||
case "gitStatus":
|
||||
data[f] = map[string]interface{}{
|
||||
"ref": c.GitStatus.Ref,
|
||||
"hasUnpushedChanges": c.GitStatus.HasUnpushedChanges,
|
||||
"hasUncommitedChanges": c.GitStatus.HasUncommitedChanges,
|
||||
}
|
||||
default:
|
||||
sf := v.FieldByNameFunc(func(s string) bool {
|
||||
return strings.EqualFold(f, s)
|
||||
})
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// ListCodespaces returns a list of codespaces for the user. Pass a negative limit to request all pages from
|
||||
// the API until all codespaces have been fetched.
|
||||
func (a *API) ListCodespaces(ctx context.Context, limit int) (codespaces []*Codespace, err error) {
|
||||
|
|
|
|||
|
|
@ -10,18 +10,6 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/liveshare"
|
||||
)
|
||||
|
||||
type logger interface {
|
||||
Print(v ...interface{}) (int, error)
|
||||
Println(v ...interface{}) (int, error)
|
||||
}
|
||||
|
||||
// TODO(josebalius): clean this up once we standardrize
|
||||
// logging for codespaces
|
||||
type liveshareLogger interface {
|
||||
Println(v ...interface{})
|
||||
Printf(f string, v ...interface{})
|
||||
}
|
||||
|
||||
func connectionReady(codespace *api.Codespace) bool {
|
||||
return codespace.Connection.SessionID != "" &&
|
||||
codespace.Connection.SessionToken != "" &&
|
||||
|
|
@ -35,13 +23,21 @@ type apiClient interface {
|
|||
StartCodespace(ctx context.Context, name string) error
|
||||
}
|
||||
|
||||
type progressIndicator interface {
|
||||
StartProgressIndicatorWithLabel(s string)
|
||||
StopProgressIndicator()
|
||||
}
|
||||
|
||||
type logger interface {
|
||||
Println(v ...interface{})
|
||||
Printf(f string, v ...interface{})
|
||||
}
|
||||
|
||||
// ConnectToLiveshare waits for a Codespace to become running,
|
||||
// and connects to it using a Live Share session.
|
||||
func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshareLogger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) {
|
||||
var startedCodespace bool
|
||||
func ConnectToLiveshare(ctx context.Context, progress progressIndicator, sessionLogger logger, apiClient apiClient, codespace *api.Codespace) (sess *liveshare.Session, err error) {
|
||||
if codespace.State != api.CodespaceStateAvailable {
|
||||
startedCodespace = true
|
||||
log.Print("Starting your codespace...")
|
||||
progress.StartProgressIndicatorWithLabel("Starting codespace")
|
||||
if err := apiClient.StartCodespace(ctx, codespace.Name); err != nil {
|
||||
return nil, fmt.Errorf("error starting codespace: %w", err)
|
||||
}
|
||||
|
|
@ -49,10 +45,6 @@ func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshare
|
|||
|
||||
for retries := 0; !connectionReady(codespace); retries++ {
|
||||
if retries > 1 {
|
||||
if retries%2 == 0 {
|
||||
log.Print(".")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
|
|
@ -60,18 +52,14 @@ func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshare
|
|||
return nil, errors.New("timed out while waiting for the codespace to start")
|
||||
}
|
||||
|
||||
var err error
|
||||
codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting codespace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if startedCodespace {
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
log.Println("Connecting to your codespace...")
|
||||
progress.StartProgressIndicatorWithLabel("Connecting to codespace")
|
||||
defer progress.StopProgressIndicator()
|
||||
|
||||
return liveshare.Connect(ctx, liveshare.Options{
|
||||
ClientName: "gh",
|
||||
|
|
|
|||
|
|
@ -9,17 +9,21 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type printer interface {
|
||||
Printf(fmt string, v ...interface{})
|
||||
}
|
||||
|
||||
// Shell runs an interactive secure shell over an existing
|
||||
// port-forwarding session. It runs until the shell is terminated
|
||||
// (including by cancellation of the context).
|
||||
func Shell(ctx context.Context, log logger, sshArgs []string, port int, destination string, usingCustomPort bool) error {
|
||||
func Shell(ctx context.Context, p printer, sshArgs []string, port int, destination string, usingCustomPort bool) error {
|
||||
cmd, connArgs, err := newSSHCommand(ctx, port, destination, sshArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ssh command: %w", err)
|
||||
}
|
||||
|
||||
if usingCustomPort {
|
||||
log.Println("Connection Details: ssh " + destination + " " + strings.Join(connArgs, " "))
|
||||
p.Printf("Connection Details: ssh %s %s", destination, connArgs)
|
||||
}
|
||||
|
||||
return cmd.Run()
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ type PostCreateState struct {
|
|||
// PollPostCreateStates watches for state changes in a codespace,
|
||||
// and calls the supplied poller for each batch of state changes.
|
||||
// It runs until it encounters an error, including cancellation of the context.
|
||||
func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
|
||||
func PollPostCreateStates(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
|
||||
noopLogger := log.New(ioutil.Discard, "", 0)
|
||||
|
||||
session, err := ConnectToLiveshare(ctx, logger, noopLogger, apiClient, codespace)
|
||||
session, err := ConnectToLiveshare(ctx, progress, noopLogger, apiClient, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to Live Share: %w", err)
|
||||
}
|
||||
|
|
@ -58,12 +58,14 @@ func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClien
|
|||
}
|
||||
localPort := listen.Addr().(*net.TCPAddr).Port
|
||||
|
||||
logger.Println("Fetching SSH Details...")
|
||||
progress.StartProgressIndicatorWithLabel("Fetching SSH Details")
|
||||
defer progress.StopProgressIndicator()
|
||||
remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting ssh server details: %w", err)
|
||||
}
|
||||
|
||||
progress.StartProgressIndicatorWithLabel("Fetching status")
|
||||
tunnelClosed := make(chan error, 1) // buffered to avoid sender stuckness
|
||||
go func() {
|
||||
fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, false)
|
||||
|
|
@ -73,7 +75,7 @@ func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClien
|
|||
t := time.NewTicker(1 * time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
for ticks := 0; ; ticks++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
|
@ -83,6 +85,13 @@ func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClien
|
|||
|
||||
case <-t.C:
|
||||
states, err := getPostCreateOutput(ctx, localPort, sshUser)
|
||||
// There is an active progress indicator before the first tick
|
||||
// to show that we are fetching statuses.
|
||||
// Once the first tick happens, we stop the indicator and let
|
||||
// the subsequent post create states manage their own progress.
|
||||
if ticks == 0 {
|
||||
progress.StopProgressIndicator()
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("get post create output: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,12 +173,12 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
|
||||
if c.Flags().Changed("hostname") {
|
||||
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing `--hostname`: %w", err)}
|
||||
return cmdutil.FlagErrorf("error parsing `--hostname`: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Paginate && !strings.EqualFold(opts.RequestMethod, "GET") && opts.RequestPath != "graphql" {
|
||||
return &cmdutil.FlagError{Err: errors.New("the `--paginate` option is not supported for non-GET requests")}
|
||||
return cmdutil.FlagErrorf("the `--paginate` option is not supported for non-GET requests")
|
||||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
|
|
|
|||
|
|
@ -69,11 +69,11 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
|
|||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) {
|
||||
return &cmdutil.FlagError{Err: errors.New("--web or --with-token required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("--web or --with-token required when not running interactively")
|
||||
}
|
||||
|
||||
if tokenStdin && opts.Web {
|
||||
return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --with-token")}
|
||||
return cmdutil.FlagErrorf("specify only one of --web or --with-token")
|
||||
}
|
||||
|
||||
if tokenStdin {
|
||||
|
|
@ -91,7 +91,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
|
|||
|
||||
if cmd.Flags().Changed("hostname") {
|
||||
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
|
||||
return cmdutil.FlagErrorf("error parsing --hostname: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Co
|
|||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.Hostname == "" && !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--hostname required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("--hostname required when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
|
|||
opts.Interactive = opts.IO.CanPrompt()
|
||||
|
||||
if !opts.Interactive && opts.Hostname == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("--hostname required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("--hostname required when not running interactively")
|
||||
}
|
||||
|
||||
opts.MainExecutable = f.Executable()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func newCodeCmd(app *App) *cobra.Command {
|
|||
|
||||
codeCmd := &cobra.Command{
|
||||
Use: "code",
|
||||
Short: "Open a codespace in VS Code",
|
||||
Short: "Open a codespace in Visual Studio Code",
|
||||
Args: noArgsConstraint,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return app.VSCode(cmd.Context(), codespace, useInsiders)
|
||||
|
|
@ -25,7 +25,7 @@ func newCodeCmd(app *App) *cobra.Command {
|
|||
}
|
||||
|
||||
codeCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace")
|
||||
codeCmd.Flags().BoolVar(&useInsiders, "insiders", false, "Use the insiders version of VS Code")
|
||||
codeCmd.Flags().BoolVar(&useInsiders, "insiders", false, "Use the insiders version of Visual Studio Code")
|
||||
|
||||
return codeCmd
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool
|
|||
|
||||
url := vscodeProtocolURL(codespaceName, useInsiders)
|
||||
if err := open.Run(url); err != nil {
|
||||
return fmt.Errorf("error opening vscode URL %s: %s. (Is VS Code installed?)", url, err)
|
||||
return fmt.Errorf("error opening vscode URL %s: %s. (Is Visual Studio Code installed?)", url, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -16,23 +16,37 @@ import (
|
|||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
io *iostreams.IOStreams
|
||||
apiClient apiClient
|
||||
logger *output.Logger
|
||||
errLogger *log.Logger
|
||||
}
|
||||
|
||||
func NewApp(logger *output.Logger, apiClient apiClient) *App {
|
||||
func NewApp(io *iostreams.IOStreams, apiClient apiClient) *App {
|
||||
errLogger := log.New(io.ErrOut, "", 0)
|
||||
|
||||
return &App{
|
||||
io: io,
|
||||
apiClient: apiClient,
|
||||
logger: logger,
|
||||
errLogger: errLogger,
|
||||
}
|
||||
}
|
||||
|
||||
// StartProgressIndicatorWithLabel starts a progress indicator with a message.
|
||||
func (a *App) StartProgressIndicatorWithLabel(s string) {
|
||||
a.io.StartProgressIndicatorWithLabel(s)
|
||||
}
|
||||
|
||||
// StopProgressIndicator stops the progress indicator.
|
||||
func (a *App) StopProgressIndicator() {
|
||||
a.io.StopProgressIndicator()
|
||||
}
|
||||
|
||||
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
|
||||
type apiClient interface {
|
||||
GetUser(ctx context.Context) (*api.User, error)
|
||||
|
|
@ -138,6 +152,7 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
|
|||
|
||||
// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty.
|
||||
// It then fetches the codespace record with full connection details.
|
||||
// TODO(josebalius): accept a progress indicator or *App and show progress when fetching.
|
||||
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceName string) (codespace *api.Codespace, err error) {
|
||||
if codespaceName == "" {
|
||||
codespace, err = chooseCodespace(ctx, apiClient)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/internal/codespaces"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -43,32 +41,56 @@ 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)
|
||||
userCh := getUser(ctx, a.apiClient)
|
||||
|
||||
repo, err := getRepoName(opts.repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting repository name: %w", err)
|
||||
}
|
||||
branch, err := getBranchName(opts.branch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting branch name: %w", err)
|
||||
userInputs := struct {
|
||||
Repository string
|
||||
Branch string
|
||||
}{
|
||||
Repository: opts.repo,
|
||||
Branch: opts.branch,
|
||||
}
|
||||
|
||||
repository, err := a.apiClient.GetRepository(ctx, repo)
|
||||
if userInputs.Repository == "" {
|
||||
branchPrompt := "Branch (leave blank for default branch):"
|
||||
if userInputs.Branch != "" {
|
||||
branchPrompt = "Branch:"
|
||||
}
|
||||
questions := []*survey.Question{
|
||||
{
|
||||
Name: "repository",
|
||||
Prompt: &survey.Input{Message: "Repository:"},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "branch",
|
||||
Prompt: &survey.Input{
|
||||
Message: branchPrompt,
|
||||
Default: userInputs.Branch,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := ask(questions, &userInputs); err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
a.StartProgressIndicatorWithLabel("Fetching repository")
|
||||
repository, err := a.apiClient.GetRepository(ctx, userInputs.Repository)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting repository: %w", err)
|
||||
}
|
||||
|
||||
branch := userInputs.Branch
|
||||
if branch == "" {
|
||||
branch = repository.DefaultBranch
|
||||
}
|
||||
|
||||
locationResult := <-locationCh
|
||||
if locationResult.Err != nil {
|
||||
return fmt.Errorf("error getting codespace region location: %w", locationResult.Err)
|
||||
}
|
||||
|
||||
userResult := <-userCh
|
||||
if userResult.Err != nil {
|
||||
return fmt.Errorf("error getting codespace user: %w", userResult.Err)
|
||||
}
|
||||
|
||||
machine, err := getMachineName(ctx, a.apiClient, repository.ID, opts.machine, branch, locationResult.Location)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting machine type: %w", err)
|
||||
|
|
@ -77,37 +99,36 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
|
|||
return errors.New("there are no available machine types for this repository")
|
||||
}
|
||||
|
||||
a.logger.Print("Creating your codespace...")
|
||||
a.StartProgressIndicatorWithLabel("Creating codespace")
|
||||
codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{
|
||||
RepositoryID: repository.ID,
|
||||
Branch: branch,
|
||||
Machine: machine,
|
||||
Location: locationResult.Location,
|
||||
})
|
||||
a.logger.Print("\n")
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating codespace: %w", err)
|
||||
}
|
||||
|
||||
if opts.showStatus {
|
||||
if err := showStatus(ctx, a.logger, a.apiClient, userResult.User, codespace); err != nil {
|
||||
if err := a.showStatus(ctx, codespace); err != nil {
|
||||
return fmt.Errorf("show status: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
a.logger.Printf("Codespace created: ")
|
||||
|
||||
fmt.Fprintln(os.Stdout, codespace.Name)
|
||||
|
||||
fmt.Fprintln(a.io.Out, codespace.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// showStatus polls the codespace for a list of post create states and their status. It will keep polling
|
||||
// until all states have finished. Once all states have finished, we poll once more to check if any new
|
||||
// states have been introduced and stop polling otherwise.
|
||||
func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, user *api.User, codespace *api.Codespace) error {
|
||||
var lastState codespaces.PostCreateState
|
||||
var breakNextState bool
|
||||
func (a *App) showStatus(ctx context.Context, codespace *api.Codespace) error {
|
||||
var (
|
||||
lastState codespaces.PostCreateState
|
||||
breakNextState bool
|
||||
)
|
||||
|
||||
finishedStates := make(map[string]bool)
|
||||
ctx, stopPolling := context.WithCancel(ctx)
|
||||
|
|
@ -121,26 +142,24 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us
|
|||
}
|
||||
|
||||
if state.Name != lastState.Name {
|
||||
log.Print(state.Name)
|
||||
a.StartProgressIndicatorWithLabel(state.Name)
|
||||
|
||||
if state.Status == codespaces.PostCreateStateRunning {
|
||||
inProgress = true
|
||||
lastState = state
|
||||
log.Print("...")
|
||||
break
|
||||
}
|
||||
|
||||
finishedStates[state.Name] = true
|
||||
log.Println("..." + state.Status)
|
||||
a.StopProgressIndicator()
|
||||
} else {
|
||||
if state.Status == codespaces.PostCreateStateRunning {
|
||||
inProgress = true
|
||||
log.Print(".")
|
||||
break
|
||||
}
|
||||
|
||||
finishedStates[state.Name] = true
|
||||
log.Println(state.Status)
|
||||
a.StopProgressIndicator()
|
||||
lastState = codespaces.PostCreateState{} // reset the value
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +173,7 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us
|
|||
}
|
||||
}
|
||||
|
||||
err := codespaces.PollPostCreateStates(ctx, log, apiClient, codespace, poller)
|
||||
err := codespaces.PollPostCreateStates(ctx, a, a.apiClient, codespace, poller)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) && breakNextState {
|
||||
return nil // we cancelled the context to stop polling, we can ignore the error
|
||||
|
|
@ -166,21 +185,6 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us
|
|||
return nil
|
||||
}
|
||||
|
||||
type getUserResult struct {
|
||||
User *api.User
|
||||
Err error
|
||||
}
|
||||
|
||||
// getUser fetches the user record associated with the GITHUB_TOKEN
|
||||
func getUser(ctx context.Context, apiClient apiClient) <-chan getUserResult {
|
||||
ch := make(chan getUserResult, 1)
|
||||
go func() {
|
||||
user, err := apiClient.GetUser(ctx)
|
||||
ch <- getUserResult{user, err}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
type locationResult struct {
|
||||
Location string
|
||||
Err error
|
||||
|
|
@ -196,40 +200,6 @@ func getLocation(ctx context.Context, apiClient apiClient) <-chan locationResult
|
|||
return ch
|
||||
}
|
||||
|
||||
// getRepoName prompts the user for the name of the repository, or returns the repository if non-empty.
|
||||
func getRepoName(repo string) (string, error) {
|
||||
if repo != "" {
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
repoSurvey := []*survey.Question{
|
||||
{
|
||||
Name: "repository",
|
||||
Prompt: &survey.Input{Message: "Repository:"},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
err := ask(repoSurvey, &repo)
|
||||
return repo, err
|
||||
}
|
||||
|
||||
// getBranchName prompts the user for the name of the branch, or returns the branch if non-empty.
|
||||
func getBranchName(branch string) (string, error) {
|
||||
if branch != "" {
|
||||
return branch, nil
|
||||
}
|
||||
|
||||
branchSurvey := []*survey.Question{
|
||||
{
|
||||
Name: "branch",
|
||||
Prompt: &survey.Input{Message: "Branch:"},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
err := ask(branchSurvey, &branch)
|
||||
return branch, err
|
||||
}
|
||||
|
||||
// getMachineName prompts the user to select the machine type, or validates the machine if non-empty.
|
||||
func getMachineName(ctx context.Context, apiClient apiClient, repoID int, machine, branch, location string) (string, error) {
|
||||
machines, err := apiClient.GetCodespacesMachines(ctx, repoID, branch, location)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
var codespaces []*api.Codespace
|
||||
nameFilter := opts.codespaceName
|
||||
if nameFilter == "" {
|
||||
a.StartProgressIndicatorWithLabel("Fetching codespaces")
|
||||
codespaces, err = a.apiClient.ListCodespaces(ctx, -1)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
|
|
@ -75,7 +77,9 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
nameFilter = c.Name
|
||||
}
|
||||
} else {
|
||||
a.StartProgressIndicatorWithLabel("Fetching codespace")
|
||||
codespace, err := a.apiClient.GetCodespace(ctx, nameFilter, false)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching codespace information: %w", err)
|
||||
}
|
||||
|
|
@ -117,12 +121,19 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
return errors.New("no codespaces to delete")
|
||||
}
|
||||
|
||||
g := errgroup.Group{}
|
||||
progressLabel := "Deleting codespace"
|
||||
if len(codespacesToDelete) > 1 {
|
||||
progressLabel = "Deleting codespaces"
|
||||
}
|
||||
a.StartProgressIndicatorWithLabel(progressLabel)
|
||||
defer a.StopProgressIndicator()
|
||||
|
||||
var g errgroup.Group
|
||||
for _, c := range codespacesToDelete {
|
||||
codespaceName := c.Name
|
||||
g.Go(func() error {
|
||||
if err := a.apiClient.DeleteCodespace(ctx, codespaceName); err != nil {
|
||||
_, _ = a.logger.Errorf("error deleting codespace %q: %v\n", codespaceName, err)
|
||||
a.errLogger.Printf("error deleting codespace %q: %v\n", codespaceName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
@ -132,13 +143,6 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
if err := g.Wait(); err != nil {
|
||||
return errors.New("some codespaces failed to delete")
|
||||
}
|
||||
|
||||
noun := "Codespace"
|
||||
if len(codespacesToDelete) > 1 {
|
||||
noun = noun + "s"
|
||||
}
|
||||
a.logger.Println(noun + " deleted.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -12,7 +11,7 @@ import (
|
|||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
)
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
|
|
@ -44,7 +43,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc"},
|
||||
wantStdout: "Codespace deleted.\n",
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "by repo",
|
||||
|
|
@ -72,7 +71,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"},
|
||||
wantStdout: "Codespaces deleted.\n",
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "unused",
|
||||
|
|
@ -95,7 +94,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
|
||||
wantStdout: "Codespaces deleted.\n",
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "deletion failed",
|
||||
|
|
@ -151,7 +150,7 @@ func TestDelete(t *testing.T) {
|
|||
"Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true,
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
|
||||
wantStdout: "Codespaces deleted.\n",
|
||||
wantStdout: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
@ -188,12 +187,10 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
app := &App{
|
||||
apiClient: apiMock,
|
||||
logger: output.NewLogger(stdout, stderr, false),
|
||||
}
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStdoutTTY(true)
|
||||
app := NewApp(io, apiMock)
|
||||
err := app.Delete(context.Background(), opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ package codespace
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newListCmd(app *App) *cobra.Command {
|
||||
var asJSON bool
|
||||
var limit int
|
||||
var exporter cmdutil.Exporter
|
||||
|
||||
listCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
|
|
@ -20,38 +21,74 @@ func newListCmd(app *App) *cobra.Command {
|
|||
Args: noArgsConstraint,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if limit < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", limit)}
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", limit)
|
||||
}
|
||||
|
||||
return app.List(cmd.Context(), asJSON, limit)
|
||||
return app.List(cmd.Context(), limit, exporter)
|
||||
},
|
||||
}
|
||||
|
||||
listCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
|
||||
listCmd.Flags().IntVarP(&limit, "limit", "L", 30, "Maximum number of codespaces to list")
|
||||
cmdutil.AddJSONFlags(listCmd, &exporter, api.CodespaceFields)
|
||||
|
||||
return listCmd
|
||||
}
|
||||
|
||||
func (a *App) List(ctx context.Context, asJSON bool, limit int) error {
|
||||
func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) error {
|
||||
a.StartProgressIndicatorWithLabel("Fetching codespaces")
|
||||
codespaces, err := a.apiClient.ListCodespaces(ctx, limit)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
|
||||
table := output.NewTable(os.Stdout, asJSON)
|
||||
table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"})
|
||||
for _, apiCodespace := range codespaces {
|
||||
cs := codespace{apiCodespace}
|
||||
table.Append([]string{
|
||||
cs.Name,
|
||||
cs.Repository.FullName,
|
||||
cs.branchWithGitStatus(),
|
||||
cs.State,
|
||||
cs.CreatedAt,
|
||||
})
|
||||
if err := a.io.StartPager(); err != nil {
|
||||
a.errLogger.Printf("error starting pager: %v", err)
|
||||
}
|
||||
defer a.io.StopPager()
|
||||
|
||||
if exporter != nil {
|
||||
return exporter.Write(a.io, codespaces)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
return nil
|
||||
tp := utils.NewTablePrinter(a.io)
|
||||
if tp.IsTTY() {
|
||||
tp.AddField("NAME", nil, nil)
|
||||
tp.AddField("REPOSITORY", nil, nil)
|
||||
tp.AddField("BRANCH", nil, nil)
|
||||
tp.AddField("STATE", nil, nil)
|
||||
tp.AddField("CREATED AT", nil, nil)
|
||||
tp.EndRow()
|
||||
}
|
||||
|
||||
cs := a.io.ColorScheme()
|
||||
for _, apiCodespace := range codespaces {
|
||||
c := codespace{apiCodespace}
|
||||
|
||||
var stateColor func(string) string
|
||||
switch c.State {
|
||||
case api.CodespaceStateStarting:
|
||||
stateColor = cs.Yellow
|
||||
case api.CodespaceStateAvailable:
|
||||
stateColor = cs.Green
|
||||
}
|
||||
|
||||
tp.AddField(c.Name, nil, cs.Yellow)
|
||||
tp.AddField(c.Repository.FullName, nil, nil)
|
||||
tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan)
|
||||
tp.AddField(c.State, nil, stateColor)
|
||||
|
||||
if tp.IsTTY() {
|
||||
ct, err := time.Parse(time.RFC3339, c.CreatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing date %q: %w", c.CreatedAt, err)
|
||||
}
|
||||
tp.AddField(utils.FuzzyAgoAbbr(time.Now(), ct), nil, cs.Gray)
|
||||
} else {
|
||||
tp.AddField(c.CreatedAt, nil, nil)
|
||||
}
|
||||
tp.EndRow()
|
||||
}
|
||||
|
||||
return tp.Render()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
|
|||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get or choose codespace: %w", err)
|
||||
}
|
||||
|
||||
user, err := a.apiClient.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting user: %w", err)
|
||||
|
|
@ -46,12 +51,7 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
|
|||
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
|
||||
}()
|
||||
|
||||
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get or choose codespace: %w", err)
|
||||
}
|
||||
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace)
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to Live Share: %w", err)
|
||||
}
|
||||
|
|
@ -69,8 +69,9 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
|
|||
defer listen.Close()
|
||||
localPort := listen.Addr().(*net.TCPAddr).Port
|
||||
|
||||
a.logger.Println("Fetching SSH Details...")
|
||||
a.StartProgressIndicatorWithLabel("Fetching SSH Details")
|
||||
remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting ssh server details: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type jsonwriter struct {
|
||||
w io.Writer
|
||||
pretty bool
|
||||
cols []string
|
||||
data []interface{}
|
||||
}
|
||||
|
||||
func (j *jsonwriter) SetHeader(cols []string) {
|
||||
j.cols = cols
|
||||
}
|
||||
|
||||
func (j *jsonwriter) Append(values []string) {
|
||||
row := make(map[string]string)
|
||||
for i, v := range values {
|
||||
row[camelize(j.cols[i])] = v
|
||||
}
|
||||
j.data = append(j.data, row)
|
||||
}
|
||||
|
||||
func (j *jsonwriter) Render() {
|
||||
enc := json.NewEncoder(j.w)
|
||||
if j.pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
_ = enc.Encode(j.data)
|
||||
}
|
||||
|
||||
func camelize(s string) string {
|
||||
var b strings.Builder
|
||||
capitalizeNext := false
|
||||
for i, r := range s {
|
||||
if r == ' ' {
|
||||
capitalizeNext = true
|
||||
continue
|
||||
}
|
||||
if capitalizeNext {
|
||||
b.WriteRune(unicode.ToUpper(r))
|
||||
capitalizeNext = false
|
||||
} else if i == 0 {
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type Table interface {
|
||||
SetHeader([]string)
|
||||
Append([]string)
|
||||
Render()
|
||||
}
|
||||
|
||||
func NewTable(w io.Writer, asJSON bool) Table {
|
||||
isTTY := isTTY(w)
|
||||
if asJSON {
|
||||
return &jsonwriter{w: w, pretty: isTTY}
|
||||
}
|
||||
if isTTY {
|
||||
return tablewriter.NewWriter(w)
|
||||
}
|
||||
return &tabwriter{w: w}
|
||||
}
|
||||
|
||||
func isTTY(w io.Writer) bool {
|
||||
f, ok := w.(*os.File)
|
||||
return ok && term.IsTerminal(int(f.Fd()))
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type tabwriter struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (j *tabwriter) SetHeader([]string) {}
|
||||
|
||||
func (j *tabwriter) Append(values []string) {
|
||||
var sep string
|
||||
for i, v := range values {
|
||||
if i == 1 {
|
||||
sep = "\t"
|
||||
}
|
||||
fmt.Fprintf(j.w, "%s%s", sep, v)
|
||||
}
|
||||
fmt.Fprint(j.w, "\n")
|
||||
}
|
||||
|
||||
func (j *tabwriter) Render() {}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// NewLogger returns a Logger that will write to the given stdout/stderr writers.
|
||||
// Disable the Logger to prevent it from writing to stdout in a TTY environment.
|
||||
func NewLogger(stdout, stderr io.Writer, disabled bool) *Logger {
|
||||
enabled := !disabled
|
||||
if isTTY(stdout) && !enabled {
|
||||
enabled = false
|
||||
}
|
||||
return &Logger{
|
||||
out: stdout,
|
||||
errout: stderr,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// Logger writes to the given stdout/stderr writers.
|
||||
// If not enabled, Print functions will noop but Error functions will continue
|
||||
// to write to the stderr writer.
|
||||
type Logger struct {
|
||||
mu sync.Mutex // guards the writers
|
||||
out io.Writer
|
||||
errout io.Writer
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// Print writes the arguments to the stdout writer.
|
||||
func (l *Logger) Print(v ...interface{}) (int, error) {
|
||||
if !l.enabled {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return fmt.Fprint(l.out, v...)
|
||||
}
|
||||
|
||||
// Println writes the arguments to the stdout writer with a newline at the end.
|
||||
func (l *Logger) Println(v ...interface{}) (int, error) {
|
||||
if !l.enabled {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return fmt.Fprintln(l.out, v...)
|
||||
}
|
||||
|
||||
// Printf writes the formatted arguments to the stdout writer.
|
||||
func (l *Logger) Printf(f string, v ...interface{}) (int, error) {
|
||||
if !l.enabled {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return fmt.Fprintf(l.out, f, v...)
|
||||
}
|
||||
|
||||
// Errorf writes the formatted arguments to the stderr writer.
|
||||
func (l *Logger) Errorf(f string, v ...interface{}) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return fmt.Fprintf(l.errout, f, v...)
|
||||
}
|
||||
|
||||
// Errorln writes the arguments to the stderr writer with a newline at the end.
|
||||
func (l *Logger) Errorln(v ...interface{}) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return fmt.Fprintln(l.errout, v...)
|
||||
}
|
||||
|
|
@ -7,14 +7,14 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/codespaces"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/liveshare"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/muhammadmuzzammil1998/jsonc"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
|
@ -23,22 +23,20 @@ import (
|
|||
// newPortsCmd returns a Cobra "ports" command that displays a table of available ports,
|
||||
// according to the specified flags.
|
||||
func newPortsCmd(app *App) *cobra.Command {
|
||||
var (
|
||||
codespace string
|
||||
asJSON bool
|
||||
)
|
||||
var codespace string
|
||||
var exporter cmdutil.Exporter
|
||||
|
||||
portsCmd := &cobra.Command{
|
||||
Use: "ports",
|
||||
Short: "List ports in a codespace",
|
||||
Args: noArgsConstraint,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return app.ListPorts(cmd.Context(), codespace, asJSON)
|
||||
return app.ListPorts(cmd.Context(), codespace, exporter)
|
||||
},
|
||||
}
|
||||
|
||||
portsCmd.PersistentFlags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace")
|
||||
portsCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
|
||||
cmdutil.AddJSONFlags(portsCmd, &exporter, portFields)
|
||||
|
||||
portsCmd.AddCommand(newPortsForwardCmd(app))
|
||||
portsCmd.AddCommand(newPortsVisibilityCmd(app))
|
||||
|
|
@ -47,7 +45,7 @@ func newPortsCmd(app *App) *cobra.Command {
|
|||
}
|
||||
|
||||
// ListPorts lists known ports in a codespace.
|
||||
func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) (err error) {
|
||||
func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) {
|
||||
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
|
||||
if err != nil {
|
||||
// TODO(josebalius): remove special handling of this error here and it other places
|
||||
|
|
@ -59,14 +57,15 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool)
|
|||
|
||||
devContainerCh := getDevContainer(ctx, a.apiClient, codespace)
|
||||
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace)
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to Live Share: %w", err)
|
||||
}
|
||||
defer safeClose(session, &err)
|
||||
|
||||
a.logger.Println("Loading ports...")
|
||||
a.StartProgressIndicatorWithLabel("Fetching ports")
|
||||
ports, err := session.GetSharedServers(ctx)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting ports of shared servers: %w", err)
|
||||
}
|
||||
|
|
@ -74,30 +73,97 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool)
|
|||
devContainerResult := <-devContainerCh
|
||||
if devContainerResult.err != nil {
|
||||
// Warn about failure to read the devcontainer file. Not a codespace command error.
|
||||
_, _ = a.logger.Errorf("Failed to get port names: %v\n", devContainerResult.err.Error())
|
||||
a.errLogger.Printf("Failed to get port names: %v", devContainerResult.err.Error())
|
||||
}
|
||||
|
||||
table := output.NewTable(os.Stdout, asJSON)
|
||||
table.SetHeader([]string{"Label", "Port", "Visibility", "Browse URL"})
|
||||
for _, port := range ports {
|
||||
sourcePort := strconv.Itoa(port.SourcePort)
|
||||
var portName string
|
||||
if devContainerResult.devContainer != nil {
|
||||
if attributes, ok := devContainerResult.devContainer.PortAttributes[sourcePort]; ok {
|
||||
portName = attributes.Label
|
||||
}
|
||||
portInfos := make([]*portInfo, len(ports))
|
||||
for i, p := range ports {
|
||||
portInfos[i] = &portInfo{
|
||||
Port: p,
|
||||
codespace: codespace,
|
||||
devContainer: devContainerResult.devContainer,
|
||||
}
|
||||
|
||||
table.Append([]string{
|
||||
portName,
|
||||
sourcePort,
|
||||
port.Privacy,
|
||||
fmt.Sprintf("https://%s-%s.githubpreview.dev/", codespace.Name, sourcePort),
|
||||
})
|
||||
}
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
if err := a.io.StartPager(); err != nil {
|
||||
a.errLogger.Printf("error starting pager: %v", err)
|
||||
}
|
||||
defer a.io.StopPager()
|
||||
|
||||
if exporter != nil {
|
||||
return exporter.Write(a.io, portInfos)
|
||||
}
|
||||
|
||||
cs := a.io.ColorScheme()
|
||||
tp := utils.NewTablePrinter(a.io)
|
||||
|
||||
if tp.IsTTY() {
|
||||
tp.AddField("LABEL", nil, nil)
|
||||
tp.AddField("PORT", nil, nil)
|
||||
tp.AddField("VISIBILITY", nil, nil)
|
||||
tp.AddField("BROWSE URL", nil, nil)
|
||||
tp.EndRow()
|
||||
}
|
||||
|
||||
for _, port := range portInfos {
|
||||
tp.AddField(port.Label(), nil, nil)
|
||||
tp.AddField(strconv.Itoa(port.SourcePort), nil, cs.Yellow)
|
||||
tp.AddField(port.Privacy, nil, nil)
|
||||
tp.AddField(port.BrowseURL(), nil, nil)
|
||||
tp.EndRow()
|
||||
}
|
||||
return tp.Render()
|
||||
}
|
||||
|
||||
type portInfo struct {
|
||||
*liveshare.Port
|
||||
codespace *api.Codespace
|
||||
devContainer *devContainer
|
||||
}
|
||||
|
||||
func (pi *portInfo) BrowseURL() string {
|
||||
return fmt.Sprintf("https://%s-%d.githubpreview.dev", pi.codespace.Name, pi.Port.SourcePort)
|
||||
}
|
||||
|
||||
func (pi *portInfo) Label() string {
|
||||
if pi.devContainer != nil {
|
||||
portStr := strconv.Itoa(pi.Port.SourcePort)
|
||||
if attributes, ok := pi.devContainer.PortAttributes[portStr]; ok {
|
||||
return attributes.Label
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var portFields = []string{
|
||||
"sourcePort",
|
||||
// "destinationPort", // TODO(mislav): this appears to always be blank?
|
||||
"visibility",
|
||||
"label",
|
||||
"browseUrl",
|
||||
}
|
||||
|
||||
func (pi *portInfo) ExportData(fields []string) map[string]interface{} {
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "sourcePort":
|
||||
data[f] = pi.Port.SourcePort
|
||||
case "destinationPort":
|
||||
data[f] = pi.Port.DestinationPort
|
||||
case "visibility":
|
||||
data[f] = pi.Port.Privacy
|
||||
case "label":
|
||||
data[f] = pi.Label()
|
||||
case "browseUrl":
|
||||
data[f] = pi.BrowseURL()
|
||||
default:
|
||||
panic("unkown field: " + f)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type devContainerResult struct {
|
||||
|
|
@ -168,6 +234,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar
|
|||
if err != nil {
|
||||
return fmt.Errorf("error parsing port arguments: %w", err)
|
||||
}
|
||||
|
||||
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
|
||||
if err != nil {
|
||||
if err == errNoCodespaces {
|
||||
|
|
@ -176,18 +243,20 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar
|
|||
return fmt.Errorf("error getting codespace: %w", err)
|
||||
}
|
||||
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace)
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to Live Share: %w", err)
|
||||
}
|
||||
defer safeClose(session, &err)
|
||||
|
||||
// TODO: check if port visibility can be updated in parallel instead of sequentially
|
||||
for _, port := range ports {
|
||||
if err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility); err != nil {
|
||||
a.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating port %d visibility to: %s", port.number, port.visibility))
|
||||
err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error update port to public: %w", err)
|
||||
}
|
||||
|
||||
a.logger.Printf("Port %d is now %s scoped.\n", port.number, port.visibility)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -250,7 +319,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st
|
|||
return fmt.Errorf("error getting codespace: %w", err)
|
||||
}
|
||||
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace)
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to Live Share: %w", err)
|
||||
}
|
||||
|
|
@ -267,7 +336,8 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st
|
|||
return err
|
||||
}
|
||||
defer listen.Close()
|
||||
a.logger.Printf("Forwarding ports: remote %d <=> local %d\n", pair.remote, pair.local)
|
||||
|
||||
a.errLogger.Printf("Forwarding ports: remote %d <=> local %d", pair.remote, pair.local)
|
||||
name := fmt.Sprintf("share-%d", pair.remote)
|
||||
fwd := liveshare.NewPortForwarder(session, name, pair.remote, false)
|
||||
return fwd.ForwardToListener(ctx, listen) // error always non-nil
|
||||
|
|
|
|||
|
|
@ -1,23 +1,13 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRootCmd(app *App) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "codespace",
|
||||
SilenceUsage: true, // don't print usage message after each error (see #80)
|
||||
SilenceErrors: false, // print errors automatically so that main need not
|
||||
Short: "List, create, delete and SSH into codespaces",
|
||||
Long: `Work with GitHub codespaces`,
|
||||
Example: heredoc.Doc(`
|
||||
$ gh codespace list
|
||||
$ gh codespace create
|
||||
$ gh codespace delete
|
||||
$ gh codespace ssh
|
||||
`),
|
||||
Use: "codespace",
|
||||
Short: "Connect to and manage your codespaces",
|
||||
}
|
||||
|
||||
root.AddCommand(newCodeCmd(app))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/codespaces"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/liveshare"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -30,11 +31,12 @@ func newSSHCmd(app *App) *cobra.Command {
|
|||
var opts sshOptions
|
||||
|
||||
sshCmd := &cobra.Command{
|
||||
Use: "ssh [flags] [--] [ssh-flags] [command]",
|
||||
Use: "ssh [<flags>...] [-- <ssh-flags>...] [<command>]",
|
||||
Short: "SSH into a codespace",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return app.SSH(cmd.Context(), args, opts)
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
sshCmd.Flags().StringVarP(&opts.profile, "profile", "", "", "Name of the SSH profile to use")
|
||||
|
|
@ -52,6 +54,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get or choose codespace: %w", err)
|
||||
}
|
||||
|
||||
// TODO(josebalius): We can fetch the user in parallel to everything else
|
||||
// we should convert this call and others to happen async
|
||||
user, err := a.apiClient.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting user: %w", err)
|
||||
|
|
@ -62,11 +71,6 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
|
||||
}()
|
||||
|
||||
codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get or choose codespace: %w", err)
|
||||
}
|
||||
|
||||
liveshareLogger := noopLogger()
|
||||
if opts.debug {
|
||||
debugLogger, err := newFileLogger(opts.debugFile)
|
||||
|
|
@ -76,10 +80,10 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
defer safeClose(debugLogger, &err)
|
||||
|
||||
liveshareLogger = debugLogger.Logger
|
||||
a.logger.Println("Debug file located at: " + debugLogger.Name())
|
||||
a.errLogger.Printf("Debug file located at: %s", debugLogger.Name())
|
||||
}
|
||||
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, liveshareLogger, a.apiClient, codespace)
|
||||
session, err := codespaces.ConnectToLiveshare(ctx, a, liveshareLogger, a.apiClient, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to Live Share: %w", err)
|
||||
}
|
||||
|
|
@ -89,8 +93,9 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
return err
|
||||
}
|
||||
|
||||
a.logger.Println("Fetching SSH Details...")
|
||||
a.StartProgressIndicatorWithLabel("Fetching SSH Details")
|
||||
remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting ssh server details: %w", err)
|
||||
}
|
||||
|
|
@ -113,7 +118,6 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
connectDestination = fmt.Sprintf("%s@localhost", sshUser)
|
||||
}
|
||||
|
||||
a.logger.Println("Ready...")
|
||||
tunnelClosed := make(chan error, 1)
|
||||
go func() {
|
||||
fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, true)
|
||||
|
|
@ -126,7 +130,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
if opts.scpArgs != nil {
|
||||
err = codespaces.Copy(ctx, opts.scpArgs, localSSHServerPort, connectDestination)
|
||||
} else {
|
||||
err = codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort)
|
||||
err = codespaces.Shell(ctx, a.errLogger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort)
|
||||
}
|
||||
shellClosed <- err
|
||||
}()
|
||||
|
|
@ -193,7 +197,7 @@ users; see https://lwn.net/Articles/835962/ for discussion.
|
|||
|
||||
// Copy copies files between the local and remote file systems.
|
||||
// The mechanics are similar to 'ssh' but using 'scp'.
|
||||
func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err error) {
|
||||
func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("cp requires source and destination arguments")
|
||||
}
|
||||
|
|
@ -201,8 +205,10 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err erro
|
|||
opts.scpArgs = append(opts.scpArgs, "-r")
|
||||
}
|
||||
opts.scpArgs = append(opts.scpArgs, "--")
|
||||
hasRemote := false
|
||||
for _, arg := range args {
|
||||
if rest := strings.TrimPrefix(arg, "remote:"); rest != arg {
|
||||
hasRemote = true
|
||||
// scp treats each filename argument as a shell expression,
|
||||
// subjecting it to expansion of environment variables, braces,
|
||||
// tilde, backticks, globs and so on. Because these present a
|
||||
|
|
@ -225,6 +231,9 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err erro
|
|||
}
|
||||
opts.scpArgs = append(opts.scpArgs, arg)
|
||||
}
|
||||
if !hasRemote {
|
||||
return cmdutil.FlagErrorf("at least one argument must have a 'remote:' prefix")
|
||||
}
|
||||
return a.SSH(ctx, nil, opts.sshOptions)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ func newStopCmd(app *App) *cobra.Command {
|
|||
|
||||
func (a *App) StopCodespace(ctx context.Context, codespaceName string) error {
|
||||
if codespaceName == "" {
|
||||
a.StartProgressIndicatorWithLabel("Fetching codespaces")
|
||||
codespaces, err := a.apiClient.ListCodespaces(ctx, -1)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list codespaces: %w", err)
|
||||
}
|
||||
|
|
@ -49,7 +51,9 @@ func (a *App) StopCodespace(ctx context.Context, codespaceName string) error {
|
|||
}
|
||||
codespaceName = codespace.Name
|
||||
} else {
|
||||
a.StartProgressIndicatorWithLabel("Fetching codespace")
|
||||
c, err := a.apiClient.GetCodespace(ctx, codespaceName, false)
|
||||
a.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get codespace: %q: %w", codespaceName, err)
|
||||
}
|
||||
|
|
@ -59,10 +63,11 @@ func (a *App) StopCodespace(ctx context.Context, codespaceName string) error {
|
|||
}
|
||||
}
|
||||
|
||||
a.StartProgressIndicatorWithLabel("Stopping codespace")
|
||||
defer a.StopProgressIndicator()
|
||||
if err := a.apiClient.StopCodespace(ctx, codespaceName); err != nil {
|
||||
return fmt.Errorf("failed to stop codespace: %w", err)
|
||||
}
|
||||
a.logger.Println("Codespace stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package completion
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -68,7 +67,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if shellType == "" {
|
||||
if io.IsStdoutTTY() {
|
||||
return &cmdutil.FlagError{Err: errors.New("error: the value for `--shell` is required")}
|
||||
return cmdutil.FlagErrorf("error: the value for `--shell` is required")
|
||||
}
|
||||
shellType = "bash"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,13 +117,13 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
Short: "Upgrade installed extensions",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 && !flagAll {
|
||||
return &cmdutil.FlagError{Err: errors.New("must specify an extension to upgrade")}
|
||||
return cmdutil.FlagErrorf("must specify an extension to upgrade")
|
||||
}
|
||||
if len(args) > 0 && flagAll {
|
||||
return &cmdutil.FlagError{Err: errors.New("cannot use `--all` with extension name")}
|
||||
return cmdutil.FlagErrorf("cannot use `--all` with extension name")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return &cmdutil.FlagError{Err: errors.New("too many arguments")}
|
||||
return cmdutil.FlagErrorf("too many arguments")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
|
|||
|
|
@ -48,3 +48,7 @@ func (e *Extension) UpdateAvailable() bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *Extension) IsBinary() bool {
|
||||
return e.kind == BinaryKind
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,9 +69,11 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
forwardArgs := args[1:]
|
||||
|
||||
exts, _ := m.list(false)
|
||||
var ext Extension
|
||||
for _, e := range exts {
|
||||
if e.Name() == extName {
|
||||
exe = e.Path()
|
||||
ext = e
|
||||
exe = ext.Path()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +83,9 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
|
||||
var externalCmd *exec.Cmd
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if ext.IsBinary() || runtime.GOOS != "windows" {
|
||||
externalCmd = m.newCommand(exe, forwardArgs...)
|
||||
} else if runtime.GOOS == "windows" {
|
||||
// Dispatch all extension calls through the `sh` interpreter to support executable files with a
|
||||
// shebang line on Windows.
|
||||
shExe, err := m.findSh()
|
||||
|
|
@ -93,8 +97,6 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
}
|
||||
forwardArgs = append([]string{"-c", `command "$@"`, "--", exe}, forwardArgs...)
|
||||
externalCmd = m.newCommand(shExe, forwardArgs...)
|
||||
} else {
|
||||
externalCmd = m.newCommand(exe, forwardArgs...)
|
||||
}
|
||||
externalCmd.Stdin = stdin
|
||||
externalCmd.Stdout = stdout
|
||||
|
|
@ -268,7 +270,17 @@ func (m *Manager) getLatestVersion(ext Extension) (string, error) {
|
|||
if ext.isLocal {
|
||||
return "", fmt.Errorf("unable to get latest version for local extensions")
|
||||
}
|
||||
if ext.kind == GitKind {
|
||||
if ext.IsBinary() {
|
||||
repo, err := ghrepo.FromFullName(ext.url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := fetchLatestRelease(m.client, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Tag, nil
|
||||
} else {
|
||||
gitExe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -282,16 +294,6 @@ func (m *Manager) getLatestVersion(ext Extension) (string, error) {
|
|||
}
|
||||
remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0]
|
||||
return string(remoteSha), nil
|
||||
} else {
|
||||
repo, err := ghrepo.FromFullName(ext.url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := fetchLatestRelease(m.client, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Tag, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +479,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
|||
return upToDateError
|
||||
}
|
||||
var err error
|
||||
if ext.kind == BinaryKind {
|
||||
if ext.IsBinary() {
|
||||
err = m.upgradeBinExtension(ext)
|
||||
} else {
|
||||
err = m.upgradeGitExtension(ext, force)
|
||||
|
|
|
|||
|
|
@ -139,6 +139,30 @@ func TestManager_Dispatch(t *testing.T) {
|
|||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
func TestManager_Dispatch_binary(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
extPath := filepath.Join(tempDir, "extensions", "gh-hello")
|
||||
exePath := filepath.Join(extPath, "gh-hello")
|
||||
bm := binManifest{
|
||||
Owner: "owner",
|
||||
Name: "gh-hello",
|
||||
Host: "github.com",
|
||||
Tag: "v1.0.0",
|
||||
}
|
||||
assert.NoError(t, stubBinaryExtension(extPath, bm))
|
||||
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
found, err := m.Dispatch([]string{"hello", "one", "two"}, nil, stdout, stderr)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, found)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf("[%s one two]\n", exePath), stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
func TestManager_Remove(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm
|
|||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)}
|
||||
return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err)
|
||||
})
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
return nil
|
||||
}
|
||||
if opts.IO.IsStdinTTY() {
|
||||
return &cmdutil.FlagError{Err: errors.New("no filenames passed and nothing on STDIN")}
|
||||
return cmdutil.FlagErrorf("no filenames passed and nothing on STDIN")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -40,7 +39,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.Limit < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)}
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
}
|
||||
|
||||
opts.Visibility = "all"
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
if opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY() {
|
||||
return &cmdutil.FlagError{Err: errors.New("GPG key file missing")}
|
||||
return cmdutil.FlagErrorf("GPG key file missing")
|
||||
}
|
||||
opts.KeyFile = "-"
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
|
@ -83,13 +82,13 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
if !opts.IO.CanPrompt() && opts.RecoverFile != "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--recover` only supported when running interactively")}
|
||||
return cmdutil.FlagErrorf("`--recover` only supported when running interactively")
|
||||
}
|
||||
|
||||
opts.Interactive = !(titleProvided && bodyProvided)
|
||||
|
||||
if opts.Interactive && !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("must provide title and body when not running interactively")}
|
||||
return cmdutil.FlagErrorf("must provide title and body when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
|
@ -106,7 +105,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
if opts.Interactive && !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("field to edit flag required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("field to edit flag required when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if opts.LimitResults < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.LimitResults)}
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.LimitResults)
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package checks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
|
@ -49,7 +48,7 @@ func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Co
|
|||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
|
|
@ -39,7 +37,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
Args: cobra.MaximumNArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
var selector string
|
||||
if len(args) > 0 {
|
||||
|
|
|
|||
|
|
@ -126,11 +126,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
opts.MaintainerCanModify = !noMaintainerEdit
|
||||
|
||||
if !opts.IO.CanPrompt() && opts.RecoverFile != "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--recover` only supported when running interactively")}
|
||||
return cmdutil.FlagErrorf("`--recover` only supported when running interactively")
|
||||
}
|
||||
|
||||
if !opts.IO.CanPrompt() && !opts.WebMode && !opts.TitleProvided && !opts.Autofill {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--title` or `--fill` required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("`--title` or `--fill` required when not running interactively")
|
||||
}
|
||||
|
||||
if opts.IsDraft && opts.WebMode {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
|
|||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
|
|
@ -58,7 +58,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
if !validColorFlag(opts.UseColor) {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("did not understand color: %q. Expected one of always, never, or auto", opts.UseColor)}
|
||||
return cmdutil.FlagErrorf("did not understand color: %q. Expected one of always, never, or auto", opts.UseColor)
|
||||
}
|
||||
|
||||
if opts.UseColor == "auto" && !opts.IO.IsStdoutTTY() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
|
@ -120,7 +119,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
if opts.Interactive && !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if opts.LimitResults < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid value for --limit: %v", opts.LimitResults)}
|
||||
return cmdutil.FlagErrorf("invalid value for --limit: %v", opts.LimitResults)
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("draft") {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
|
|
@ -97,11 +97,11 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
}
|
||||
if methodFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--merge, --rebase, or --squash required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("--merge, --rebase, or --squash required when not running interactively")
|
||||
}
|
||||
opts.InteractiveMode = true
|
||||
} else if methodFlags > 1 {
|
||||
return &cmdutil.FlagError{Err: errors.New("only one of --merge, --rebase, or --squash can be enabled")}
|
||||
return cmdutil.FlagErrorf("only one of --merge, --rebase, or --squash can be enabled")
|
||||
}
|
||||
|
||||
opts.IsDeleteBranchIndicated = cmd.Flags().Changed("delete-branch")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package ready
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
|
@ -35,14 +34,14 @@ func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Comm
|
|||
Mark a pull request as ready for review
|
||||
|
||||
Without an argument, the pull request that belongs to the current branch
|
||||
is displayed.
|
||||
is marked as ready.
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
|
|||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
|
|
@ -106,26 +106,26 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
|
|||
found++
|
||||
opts.ReviewType = api.ReviewRequestChanges
|
||||
if opts.Body == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("body cannot be blank for request-changes review")}
|
||||
return cmdutil.FlagErrorf("body cannot be blank for request-changes review")
|
||||
}
|
||||
}
|
||||
if flagComment {
|
||||
found++
|
||||
opts.ReviewType = api.ReviewComment
|
||||
if opts.Body == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("body cannot be blank for comment review")}
|
||||
return cmdutil.FlagErrorf("body cannot be blank for comment review")
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 && opts.Body == "" {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--approve, --request-changes, or --comment required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("--approve, --request-changes, or --comment required when not running interactively")
|
||||
}
|
||||
opts.InteractiveMode = true
|
||||
} else if found == 0 && opts.Body != "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body unsupported without --approve, --request-changes, or --comment")}
|
||||
return cmdutil.FlagErrorf("--body unsupported without --approve, --request-changes, or --comment")
|
||||
} else if found > 1 {
|
||||
return &cmdutil.FlagError{Err: errors.New("need exactly one of --approve, --request-changes, or --comment")}
|
||||
return cmdutil.FlagErrorf("need exactly one of --approve, --request-changes, or --comment")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -65,15 +65,15 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
|
|||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--body`, `--body-file` or `--web` required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("`--body`, `--body-file` or `--web` required when not running interactively")
|
||||
}
|
||||
opts.Interactive = true
|
||||
} else if inputFlags == 1 {
|
||||
if !opts.IO.CanPrompt() && opts.InputType == InputTypeEditor {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--body`, `--body-file` or `--web` required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("`--body`, `--body-file` or `--web` required when not running interactively")
|
||||
}
|
||||
} else if inputFlags > 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`")}
|
||||
return cmdutil.FlagErrorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO(josebalius): Should we be guarding here?
|
||||
if f.progress != nil {
|
||||
f.progress.StartProgressIndicator()
|
||||
defer f.progress.StopProgressIndicator()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
|
@ -55,7 +54,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
return cmdutil.FlagErrorf("argument required when using the --repo flag")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobr
|
|||
|
||||
if len(args) == 0 {
|
||||
if len(opts.FilePatterns) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("the '--pattern' flag is required when downloading the latest release")}
|
||||
return cmdutil.FlagErrorf("the '--pattern' flag is required when downloading the latest release")
|
||||
}
|
||||
} else {
|
||||
opts.TagName = args[0]
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ type ReleaseAsset struct {
|
|||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
func (rel *Release) ExportData(fields []string) *map[string]interface{} {
|
||||
func (rel *Release) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(rel).Elem()
|
||||
fieldByName := func(v reflect.Value, field string) reflect.Value {
|
||||
return v.FieldByNameFunc(func(s string) bool {
|
||||
|
|
@ -114,7 +114,7 @@ func (rel *Release) ExportData(fields []string) *map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
return data
|
||||
}
|
||||
|
||||
// FetchRelease finds a repository release by its tagName.
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm
|
|||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)}
|
||||
return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err)
|
||||
})
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
|
@ -94,25 +93,25 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
if len(args) == 0 && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") {
|
||||
return &cmdutil.FlagError{Err: errors.New(".gitignore and license templates are added only when a specific repository name is passed")}
|
||||
return cmdutil.FlagErrorf(".gitignore and license templates are added only when a specific repository name is passed")
|
||||
}
|
||||
|
||||
if opts.Template != "" && (opts.GitIgnoreTemplate != "" || opts.LicenseTemplate != "") {
|
||||
return &cmdutil.FlagError{Err: errors.New(".gitignore and license templates are not added when template is provided")}
|
||||
return cmdutil.FlagErrorf(".gitignore and license templates are not added when template is provided")
|
||||
}
|
||||
|
||||
if !opts.IO.CanPrompt() {
|
||||
if opts.Name == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("name argument required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("name argument required when not running interactively")
|
||||
}
|
||||
|
||||
if !opts.Internal && !opts.Private && !opts.Public {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--public`, `--private`, or `--internal` required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("`--public`, `--private`, or `--internal` required when not running interactively")
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || cmd.Flags().Changed("enable-issues") || cmd.Flags().Changed("enable-wiki")) {
|
||||
return &cmdutil.FlagError{Err: errors.New("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`")}
|
||||
return cmdutil.FlagErrorf("The `--template` option is not supported with `--homepage`, `--team`, `--enable-issues`, or `--enable-wiki`")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
@ -41,8 +40,7 @@ To authorize, run "gh auth refresh -s delete_repo"`,
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.RepoArg = args[0]
|
||||
if !opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
return &cmdutil.FlagError{
|
||||
Err: errors.New("could not prompt: confirmation with prompt or --confirm flag required")}
|
||||
return cmdutil.FlagErrorf("could not prompt: confirmation with prompt or --confirm flag required")
|
||||
}
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fork
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -61,7 +60,7 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman
|
|||
Use: "fork [<repository>] [-- <gitflags>...]",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.ArgsLenAtDash() == 0 && len(args[1:]) > 0 {
|
||||
return cmdutil.FlagError{Err: fmt.Errorf("repository argument required when passing 'git clone' flags")}
|
||||
return cmdutil.FlagErrorf("repository argument required when passing 'git clone' flags")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
@ -84,11 +83,11 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`,
|
|||
}
|
||||
|
||||
if cmd.Flags().Changed("org") && opts.Organization == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("--org cannot be blank")}
|
||||
return cmdutil.FlagErrorf("--org cannot be blank")
|
||||
}
|
||||
|
||||
if opts.RemoteName == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("--remote-name cannot be blank")}
|
||||
return cmdutil.FlagErrorf("--remote-name cannot be blank")
|
||||
} else if !cmd.Flags().Changed("remote-name") {
|
||||
opts.Rename = true // Any existing 'origin' will be renamed to upstream
|
||||
}
|
||||
|
|
@ -109,7 +108,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`,
|
|||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)}
|
||||
return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err)
|
||||
})
|
||||
|
||||
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}")
|
||||
|
|
|
|||
|
|
@ -54,17 +54,17 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Short: "List repositories owned by user or organization",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
if opts.Limit < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)}
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
}
|
||||
|
||||
if flagPrivate && flagPublic {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--public` or `--private`")}
|
||||
return cmdutil.FlagErrorf("specify only one of `--public` or `--private`")
|
||||
}
|
||||
if opts.Source && opts.Fork {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--source` or `--fork`")}
|
||||
return cmdutil.FlagErrorf("specify only one of `--source` or `--fork`")
|
||||
}
|
||||
if opts.Archived && opts.NonArchived {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--archived` or `--no-archived`")}
|
||||
return cmdutil.FlagErrorf("specify only one of `--archived` or `--no-archived`")
|
||||
}
|
||||
|
||||
if flagPrivate {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func rootFlagErrorFunc(cmd *cobra.Command, err error) error {
|
|||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: err}
|
||||
return cmdutil.FlagErrorWrap(err)
|
||||
}
|
||||
|
||||
var hasFailed bool
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
authCmd "github.com/cli/cli/v2/pkg/cmd/auth"
|
||||
browseCmd "github.com/cli/cli/v2/pkg/cmd/browse"
|
||||
codespaceCmd "github.com/cli/cli/v2/pkg/cmd/codespace"
|
||||
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
|
||||
completionCmd "github.com/cli/cli/v2/pkg/cmd/completion"
|
||||
configCmd "github.com/cli/cli/v2/pkg/cmd/config"
|
||||
extensionCmd "github.com/cli/cli/v2/pkg/cmd/extension"
|
||||
|
|
@ -130,13 +129,14 @@ func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, er
|
|||
}
|
||||
|
||||
func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := codespaceCmd.NewRootCmd(codespaceCmd.NewApp(
|
||||
output.NewLogger(f.IOStreams.Out, f.IOStreams.ErrOut, !f.IOStreams.IsStdoutTTY()),
|
||||
app := codespaceCmd.NewApp(
|
||||
f.IOStreams,
|
||||
codespacesAPI.New("", &lazyLoadedHTTPClient{factory: f}),
|
||||
))
|
||||
)
|
||||
cmd := codespaceCmd.NewRootCmd(app)
|
||||
cmd.Use = "codespace"
|
||||
cmd.Aliases = []string{"cs"}
|
||||
cmd.Hidden = true
|
||||
cmd.Annotations = map[string]string{"IsCore": "true"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func NewCmdCancel(f *cmdutil.Factory, runF func(*CancelOptions) error) *cobra.Co
|
|||
if len(args) > 0 {
|
||||
opts.RunID = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("run ID required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
opts.PlainOutput = !terminal
|
||||
|
||||
if opts.Limit < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)}
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
|
|||
if len(args) > 0 {
|
||||
opts.RunID = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("run ID required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
|
||||
if len(args) == 0 && opts.JobID == "" {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("run or job ID required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("run or job ID required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
@ -135,11 +135,11 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
if opts.Web && opts.Log {
|
||||
return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --log")}
|
||||
return cmdutil.FlagErrorf("specify only one of --web or --log")
|
||||
}
|
||||
|
||||
if opts.Log && opts.LogFailed {
|
||||
return &cmdutil.FlagError{Err: errors.New("specify only one of --log or --log-failed")}
|
||||
return cmdutil.FlagErrorf("specify only one of --log or --log-failed")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package watch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
|
@ -57,7 +56,7 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm
|
|||
if len(args) > 0 {
|
||||
opts.RunID = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("run ID required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
`),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return &cmdutil.FlagError{Err: errors.New("must pass single secret name")}
|
||||
return cmdutil.FlagErrorf("must pass single secret name")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
@ -92,23 +92,19 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
|
||||
if cmd.Flags().Changed("visibility") {
|
||||
if opts.OrgName == "" {
|
||||
return &cmdutil.FlagError{Err: errors.New(
|
||||
"--visibility not supported for repository secrets; did you mean to pass --org?")}
|
||||
return cmdutil.FlagErrorf("--visibility not supported for repository secrets; did you mean to pass --org?")
|
||||
}
|
||||
|
||||
if opts.Visibility != shared.All && opts.Visibility != shared.Private && opts.Visibility != shared.Selected {
|
||||
return &cmdutil.FlagError{Err: errors.New(
|
||||
"--visibility must be one of `all`, `private`, or `selected`")}
|
||||
return cmdutil.FlagErrorf("--visibility must be one of `all`, `private`, or `selected`")
|
||||
}
|
||||
|
||||
if opts.Visibility != shared.Selected && cmd.Flags().Changed("repos") {
|
||||
return &cmdutil.FlagError{Err: errors.New(
|
||||
"--repos only supported when --visibility='selected'")}
|
||||
return cmdutil.FlagErrorf("--repos only supported when --visibility='selected'")
|
||||
}
|
||||
|
||||
if opts.Visibility == shared.Selected && !cmd.Flags().Changed("repos") {
|
||||
return &cmdutil.FlagError{Err: errors.New(
|
||||
"--repos flag required when --visibility='selected'")}
|
||||
return cmdutil.FlagErrorf("--repos flag required when --visibility='selected'")
|
||||
}
|
||||
} else {
|
||||
if cmd.Flags().Changed("repos") {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package add
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -36,7 +35,7 @@ func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
if opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY() {
|
||||
return &cmdutil.FlagError{Err: errors.New("public key file missing")}
|
||||
return cmdutil.FlagErrorf("public key file missing")
|
||||
}
|
||||
opts.KeyFile = "-"
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func NewCmdDisable(f *cmdutil.Factory, runF func(*DisableOptions) error) *cobra.
|
|||
if len(args) > 0 {
|
||||
opts.Selector = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("workflow ID or name required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("workflow ID or name required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func NewCmdEnable(f *cmdutil.Factory, runF func(*EnableOptions) error) *cobra.Co
|
|||
if len(args) > 0 {
|
||||
opts.Selector = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("workflow ID or name required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("workflow ID or name required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
opts.PlainOutput = !terminal
|
||||
|
||||
if opts.Limit < 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)}
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command
|
|||
`),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(opts.MagicFields)+len(opts.RawFields) > 0 && len(args) == 0 {
|
||||
return cmdutil.FlagError{Err: fmt.Errorf("workflow argument required when passing -f or -F")}
|
||||
return cmdutil.FlagErrorf("workflow argument required when passing -f or -F")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
@ -91,7 +91,7 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command
|
|||
if len(args) > 0 {
|
||||
opts.Selector = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("workflow ID, name, or filename required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("workflow ID, name, or filename required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
|
@ -103,16 +103,16 @@ func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command
|
|||
}
|
||||
opts.JSONInput = string(jsonIn)
|
||||
} else if opts.JSON {
|
||||
return cmdutil.FlagError{Err: errors.New("--json specified but nothing on STDIN")}
|
||||
return cmdutil.FlagErrorf("--json specified but nothing on STDIN")
|
||||
}
|
||||
|
||||
if opts.Selector == "" {
|
||||
if opts.JSONInput != "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("workflow argument required when passing JSON")}
|
||||
return cmdutil.FlagErrorf("workflow argument required when passing JSON")
|
||||
}
|
||||
} else {
|
||||
if opts.JSON && inputFieldsPassed {
|
||||
return &cmdutil.FlagError{Err: errors.New("only one of STDIN or -f/-F can be passed")}
|
||||
return cmdutil.FlagErrorf("only one of STDIN or -f/-F can be passed")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
@ -59,13 +58,13 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
if len(args) > 0 {
|
||||
opts.Selector = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("workflow argument required when not running interactively")}
|
||||
return cmdutil.FlagErrorf("workflow argument required when not running interactively")
|
||||
} else {
|
||||
opts.Prompt = true
|
||||
}
|
||||
|
||||
if !opts.YAML && opts.Ref != "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("`--yaml` required when specifying `--ref`")}
|
||||
return cmdutil.FlagErrorf("`--yaml` required when specifying `--ref`")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package cmdutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -15,7 +14,7 @@ func MinimumArgs(n int, msg string) cobra.PositionalArgs {
|
|||
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < n {
|
||||
return &FlagError{Err: errors.New(msg)}
|
||||
return FlagErrorf("%s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -25,11 +24,11 @@ func ExactArgs(n int, msg string) cobra.PositionalArgs {
|
|||
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > n {
|
||||
return &FlagError{Err: errors.New("too many arguments")}
|
||||
return FlagErrorf("too many arguments")
|
||||
}
|
||||
|
||||
if len(args) < n {
|
||||
return &FlagError{Err: errors.New(msg)}
|
||||
return FlagErrorf("%s", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -57,5 +56,5 @@ func NoArgsQuoteReminder(cmd *cobra.Command, args []string) error {
|
|||
errMsg += "; please quote all values that have spaces"
|
||||
}
|
||||
|
||||
return &FlagError{Err: errors.New(errMsg)}
|
||||
return FlagErrorf("%s", errMsg)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,33 @@ package cmdutil
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
// FlagError is the kind of error raised in flag processing
|
||||
// FlagErrorf returns a new FlagError that wraps an error produced by
|
||||
// fmt.Errorf(format, args...).
|
||||
func FlagErrorf(format string, args ...interface{}) error {
|
||||
return FlagErrorWrap(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// FlagError returns a new FlagError that wraps the specified error.
|
||||
func FlagErrorWrap(err error) error { return &FlagError{err} }
|
||||
|
||||
// A *FlagError indicates an error processing command-line flags or other arguments.
|
||||
// Such errors cause the application to display the usage message.
|
||||
type FlagError struct {
|
||||
Err error
|
||||
// Note: not struct{error}: only *FlagError should satisfy error.
|
||||
err error
|
||||
}
|
||||
|
||||
func (fe FlagError) Error() string {
|
||||
return fe.Err.Error()
|
||||
func (fe *FlagError) Error() string {
|
||||
return fe.err.Error()
|
||||
}
|
||||
|
||||
func (fe FlagError) Unwrap() error {
|
||||
return fe.Err
|
||||
func (fe *FlagError) Unwrap() error {
|
||||
return fe.err
|
||||
}
|
||||
|
||||
// SilentError is an error that triggers exit code 1 without any error messaging
|
||||
|
|
@ -37,7 +49,7 @@ func MutuallyExclusive(message string, conditions ...bool) error {
|
|||
}
|
||||
}
|
||||
if numTrue > 1 {
|
||||
return &FlagError{Err: errors.New(message)}
|
||||
return FlagErrorf("%s", message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ func (e *exportFormat) exportData(v reflect.Value) interface{} {
|
|||
}
|
||||
|
||||
type exportable interface {
|
||||
ExportData([]string) *map[string]interface{}
|
||||
ExportData([]string) map[string]interface{}
|
||||
}
|
||||
|
||||
var exportableType = reflect.TypeOf((*exportable)(nil)).Elem()
|
||||
|
|
|
|||
|
|
@ -198,10 +198,10 @@ type exportableItem struct {
|
|||
Name string
|
||||
}
|
||||
|
||||
func (e *exportableItem) ExportData(fields []string) *map[string]interface{} {
|
||||
func (e *exportableItem) ExportData(fields []string) map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
for _, f := range fields {
|
||||
m[f] = fmt.Sprintf("%s:%s", e.Name, f)
|
||||
}
|
||||
return &m
|
||||
return m
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type Extension interface {
|
|||
URL() string
|
||||
IsLocal() bool
|
||||
UpdateAvailable() bool
|
||||
IsBinary() bool
|
||||
}
|
||||
|
||||
//go:generate moq -rm -out manager_mock.go . ExtensionManager
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ var _ Extension = &ExtensionMock{}
|
|||
//
|
||||
// // make and configure a mocked Extension
|
||||
// mockedExtension := &ExtensionMock{
|
||||
// IsBinaryFunc: func() bool {
|
||||
// panic("mock out the IsBinary method")
|
||||
// },
|
||||
// IsLocalFunc: func() bool {
|
||||
// panic("mock out the IsLocal method")
|
||||
// },
|
||||
|
|
@ -39,6 +42,9 @@ var _ Extension = &ExtensionMock{}
|
|||
//
|
||||
// }
|
||||
type ExtensionMock struct {
|
||||
// IsBinaryFunc mocks the IsBinary method.
|
||||
IsBinaryFunc func() bool
|
||||
|
||||
// IsLocalFunc mocks the IsLocal method.
|
||||
IsLocalFunc func() bool
|
||||
|
||||
|
|
@ -56,6 +62,9 @@ type ExtensionMock struct {
|
|||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// IsBinary holds details about calls to the IsBinary method.
|
||||
IsBinary []struct {
|
||||
}
|
||||
// IsLocal holds details about calls to the IsLocal method.
|
||||
IsLocal []struct {
|
||||
}
|
||||
|
|
@ -72,6 +81,7 @@ type ExtensionMock struct {
|
|||
UpdateAvailable []struct {
|
||||
}
|
||||
}
|
||||
lockIsBinary sync.RWMutex
|
||||
lockIsLocal sync.RWMutex
|
||||
lockName sync.RWMutex
|
||||
lockPath sync.RWMutex
|
||||
|
|
@ -79,6 +89,32 @@ type ExtensionMock struct {
|
|||
lockUpdateAvailable sync.RWMutex
|
||||
}
|
||||
|
||||
// IsBinary calls IsBinaryFunc.
|
||||
func (mock *ExtensionMock) IsBinary() bool {
|
||||
if mock.IsBinaryFunc == nil {
|
||||
panic("ExtensionMock.IsBinaryFunc: method is nil but Extension.IsBinary was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockIsBinary.Lock()
|
||||
mock.calls.IsBinary = append(mock.calls.IsBinary, callInfo)
|
||||
mock.lockIsBinary.Unlock()
|
||||
return mock.IsBinaryFunc()
|
||||
}
|
||||
|
||||
// IsBinaryCalls gets all the calls that were made to IsBinary.
|
||||
// Check the length with:
|
||||
// len(mockedExtension.IsBinaryCalls())
|
||||
func (mock *ExtensionMock) IsBinaryCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockIsBinary.RLock()
|
||||
calls = mock.calls.IsBinary
|
||||
mock.lockIsBinary.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// IsLocal calls IsLocalFunc.
|
||||
func (mock *ExtensionMock) IsLocal() bool {
|
||||
if mock.IsLocalFunc == nil {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
|
|
@ -37,6 +38,7 @@ type IOStreams struct {
|
|||
|
||||
progressIndicatorEnabled bool
|
||||
progressIndicator *spinner.Spinner
|
||||
progressIndicatorMu sync.Mutex
|
||||
|
||||
stdinTTYOverride bool
|
||||
stdinIsTTY bool
|
||||
|
|
@ -229,15 +231,40 @@ func (s *IOStreams) SetNeverPrompt(v bool) {
|
|||
}
|
||||
|
||||
func (s *IOStreams) StartProgressIndicator() {
|
||||
s.StartProgressIndicatorWithLabel("")
|
||||
}
|
||||
|
||||
func (s *IOStreams) StartProgressIndicatorWithLabel(label string) {
|
||||
if !s.progressIndicatorEnabled {
|
||||
return
|
||||
}
|
||||
sp := spinner.New(spinner.CharSets[11], 400*time.Millisecond, spinner.WithWriter(s.ErrOut))
|
||||
|
||||
s.progressIndicatorMu.Lock()
|
||||
defer s.progressIndicatorMu.Unlock()
|
||||
|
||||
if s.progressIndicator != nil {
|
||||
if label == "" {
|
||||
s.progressIndicator.Prefix = ""
|
||||
} else {
|
||||
s.progressIndicator.Prefix = label + " "
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// https://github.com/briandowns/spinner#available-character-sets
|
||||
dotStyle := spinner.CharSets[11]
|
||||
sp := spinner.New(dotStyle, 120*time.Millisecond, spinner.WithWriter(s.ErrOut), spinner.WithColor("fgCyan"))
|
||||
if label != "" {
|
||||
sp.Prefix = label + " "
|
||||
}
|
||||
|
||||
sp.Start()
|
||||
s.progressIndicator = sp
|
||||
}
|
||||
|
||||
func (s *IOStreams) StopProgressIndicator() {
|
||||
s.progressIndicatorMu.Lock()
|
||||
defer s.progressIndicatorMu.Unlock()
|
||||
if s.progressIndicator == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue