Merge pull request #4591 from cli/codespaces-json-export
Add export functionality to codespace commands
This commit is contained in:
commit
8d9e8e317e
8 changed files with 140 additions and 221 deletions
1
go.mod
1
go.mod
|
|
@ -26,7 +26,6 @@ require (
|
|||
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
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -177,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) {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import (
|
|||
)
|
||||
|
||||
func newListCmd(app *App) *cobra.Command {
|
||||
var asJSON bool
|
||||
var limit int
|
||||
var exporter cmdutil.Exporter
|
||||
|
||||
listCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
|
|
@ -24,17 +24,17 @@ func newListCmd(app *App) *cobra.Command {
|
|||
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()
|
||||
|
|
@ -47,6 +47,10 @@ func (a *App) List(ctx context.Context, asJSON bool, limit int) error {
|
|||
}
|
||||
defer a.io.StopPager()
|
||||
|
||||
if exporter != nil {
|
||||
return exporter.Write(a.io, codespaces)
|
||||
}
|
||||
|
||||
tp := utils.NewTablePrinter(a.io)
|
||||
if tp.IsTTY() {
|
||||
tp.AddField("NAME", nil, nil)
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
@ -12,8 +12,9 @@ import (
|
|||
|
||||
"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"
|
||||
|
|
@ -22,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))
|
||||
|
|
@ -46,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
|
||||
|
|
@ -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.errLogger.Printf("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(a.io.Out, 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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue