Merge branch 'trunk' into repo-rename

This commit is contained in:
Parth Patel 2021-10-25 17:17:06 -04:00
commit c83c6a83e6
83 changed files with 624 additions and 583 deletions

View file

@ -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 {

View file

@ -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{} {

View file

@ -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,
},

View file

@ -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

View file

@ -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/`.

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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) {

View file

@ -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",

View file

@ -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()

View file

@ -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)
}

View file

@ -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(

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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()))
}

View file

@ -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() {}

View file

@ -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...)
}

View file

@ -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

View file

@ -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))

View file

@ -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)
}

View file

@ -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
}

View file

@ -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"
}

View file

@ -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
},

View file

@ -48,3 +48,7 @@ func (e *Extension) UpdateAvailable() bool {
}
return true
}
func (e *Extension) IsBinary() bool {
return e.kind == BinaryKind
}

View file

@ -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)

View file

@ -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")))

View file

@ -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

View file

@ -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
},

View file

@ -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"

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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() {

View file

@ -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 {

View file

@ -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") {

View file

@ -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")

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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()

View file

@ -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 {

View file

@ -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]

View file

@ -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.

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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}")

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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") {

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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")
}
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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
}

View file

@ -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()

View file

@ -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
}

View file

@ -13,6 +13,7 @@ type Extension interface {
URL() string
IsLocal() bool
UpdateAvailable() bool
IsBinary() bool
}
//go:generate moq -rm -out manager_mock.go . ExtensionManager

View file

@ -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 {

View file

@ -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
}