Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Work with Azure DevOps Projects.
List the projects for an organization

```
--format string Output format: {json}
--format string Output format: {json} (default "table")
-l, --limit int Maximum number of projects to fetch (default 30)
-o, --organization string Get per-organization configuration
--state string Project state filter: {deleting|new|wellFormed|createPending|all|unchanged|deleted}
Expand All @@ -103,7 +103,7 @@ Clone a repository locally
List repositories of a project inside an organization

```
--format string Output format: {json}
--format string Output format: {json} (default "table")
--include-hidden Include hidden repositories
-L, --limit int Maximum number of repositories to list (default 30)
-o, --organization string Get per-organization configuration
Expand Down
10 changes: 5 additions & 5 deletions internal/cmd/project/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/tableprinter"
"github.com/tmeckel/azdo-cli/internal/printer"
)

type listOptions struct {
Expand Down Expand Up @@ -37,7 +37,7 @@ func NewCmdProjectList(ctx util.CmdContext) *cobra.Command {
}

cmd.Flags().StringVarP(&opts.organizationName, "organization", "o", "", "Get per-organization configuration")
util.StringEnumFlag(cmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
util.StringEnumFlag(cmd, &opts.format, "format", "", "table", []string{"json"}, "Output format")
util.StringEnumFlag(cmd, &opts.state, "state", "", "",
[]string{
string(core.ProjectStateValues.Deleting),
Expand Down Expand Up @@ -95,14 +95,14 @@ func runList(ctx util.CmdContext, opts *listOptions) (err error) {
return util.NewNoResultsError(fmt.Sprintf("No projects found for organization %s", organizationName))
}

tp, err := ctx.TablePrinter()
tp, err := ctx.Printer(opts.format)
if err != nil {
return
}

tp.HeaderRow("ID", "Name", "State")
tp.AddColumns("ID", "Name", "State")
for _, p := range res.Value {
tp.AddField(p.Id.String(), tableprinter.WithTruncate(nil))
tp.AddField(p.Id.String(), printer.WithTruncate(nil))
tp.AddField(*p.Name)
tp.AddField(string(*p.State))
tp.EndRow()
Expand Down
10 changes: 5 additions & 5 deletions internal/cmd/repo/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/tableprinter"
"github.com/tmeckel/azdo-cli/internal/printer"
)

type listOptions struct {
Expand Down Expand Up @@ -48,7 +48,7 @@ func NewCmdRepoList(ctx util.CmdContext) *cobra.Command {
cmd.Flags().StringVarP(&opts.organizationName, "organization", "o", "", "Get per-organization configuration")
cmd.Flags().IntVarP(&opts.limit, "limit", "L", 30, "Maximum number of repositories to list")
util.StringEnumFlag(cmd, &opts.visibility, "visibility", "", "", []string{"public", "private"}, "Filter by repository visibility")
util.StringEnumFlag(cmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
util.StringEnumFlag(cmd, &opts.format, "format", "", "table", []string{"json"}, "Output format")
cmd.Flags().BoolVar(&opts.includeHidden, "include-hidden", false, "Include hidden repositories")

return cmd
Expand Down Expand Up @@ -95,14 +95,14 @@ func runList(ctx util.CmdContext, opts *listOptions) (err error) {
return util.NewNoResultsError(fmt.Sprintf("No repositories found for project %s and organization %s", opts.project, organizationName))
}

tp, err := ctx.TablePrinter()
tp, err := ctx.Printer(opts.format)
if err != nil {
return
}

tp.HeaderRow("ID", "Name", "SSHUrl", "HTTPUrl")
tp.AddColumns("ID", "Name", "SSHUrl", "HTTPUrl")
for _, p := range *res {
tp.AddField(p.Id.String(), tableprinter.WithTruncate(nil))
tp.AddField(p.Id.String(), printer.WithTruncate(nil))
tp.AddField(*p.Name)
tp.AddField(*p.SshUrl)
tp.AddField(*p.WebUrl)
Expand Down
19 changes: 13 additions & 6 deletions internal/cmd/util/cmd_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"github.com/tmeckel/azdo-cli/internal/config"
"github.com/tmeckel/azdo-cli/internal/git"
"github.com/tmeckel/azdo-cli/internal/iostreams"
"github.com/tmeckel/azdo-cli/internal/printer"
"github.com/tmeckel/azdo-cli/internal/prompter"
"github.com/tmeckel/azdo-cli/internal/tableprinter"
)

type CmdContext interface {
Expand All @@ -19,7 +19,7 @@ type CmdContext interface {
Config() (config.Config, error)
Connection(organization string) (*azuredevops.Connection, error)
IOStreams() (*iostreams.IOStreams, error)
TablePrinter() (tableprinter.TablePrinter, error)
Printer(string) (printer.Printer, error)
GitClient() (*git.Client, error)
}

Expand Down Expand Up @@ -98,8 +98,15 @@ func (c *cmdContext) IOStreams() (*iostreams.IOStreams, error) {
return c.ioStreams, nil
}

func (c *cmdContext) TablePrinter() (tp tableprinter.TablePrinter, err error) {
tp, err = newTablePrinter(c.ioStreams)
func (c *cmdContext) Printer(t string) (p printer.Printer, err error) {
switch t {
case "table":
p, err = newTablePrinter(c.ioStreams)
case "json":
p, err = printer.NewJSONPrinter(c.ioStreams.Out)
default:
return nil, printer.NewUnsupportedPrinterError(t)
}
return
}

Expand Down Expand Up @@ -153,11 +160,11 @@ func newPrompter(cfg config.Config, io *iostreams.IOStreams) (p prompter.Prompte
return
}

func newTablePrinter(ios *iostreams.IOStreams) (tableprinter.TablePrinter, error) {
func newTablePrinter(ios *iostreams.IOStreams) (printer.TablePrinter, error) {
maxWidth := 80
isTTY := ios.IsStdoutTTY()
if isTTY {
maxWidth = ios.TerminalWidth()
}
return tableprinter.New(ios.Out, isTTY, maxWidth)
return printer.NewTablePrinter(ios.Out, isTTY, maxWidth)
}
59 changes: 59 additions & 0 deletions internal/printer/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package printer

import (
"encoding/json"
"io"
"time"

"github.com/tmeckel/azdo-cli/internal/text"
)

type JSONPrinter interface {
Printer
}

// NewJSONPrinter initializes a table printer with terminal mode and terminal width. When terminal mode is enabled, the
// output will be human-readable, column-formatted to fit available width, and rendered with color support.
// In non-terminal mode, the output is tab-separated and all truncation of values is disabled.
func NewJSONPrinter(w io.Writer) (jp JSONPrinter, err error) {
jp = &jsonPrinter{
out: json.NewEncoder(w),
columns: []string{},
currentColumn: -1,
rows: []map[string]string{},
}
return
}

type jsonPrinter struct {
out *json.Encoder
columns []string
currentColumn int
rows []map[string]string
}

func (jp *jsonPrinter) AddColumns(columns ...string) {
jp.columns = append(jp.columns, columns...)
}

func (jp *jsonPrinter) AddField(s string, opts ...FieldOption) {
if jp.currentColumn < 0 {
jp.rows = append(jp.rows, map[string]string{})
}
jp.currentColumn++
rowI := len(jp.rows) - 1
jp.rows[rowI][jp.columns[jp.currentColumn]] = s
}

func (jp *jsonPrinter) AddTimeField(now, t time.Time, c func(string) string) {
tf := text.FuzzyAgo(now, t)
jp.AddField(tf)
}

func (jp *jsonPrinter) EndRow() {
jp.currentColumn = -1
}

func (jp *jsonPrinter) Render() error {
return jp.out.Encode(jp.rows)
}
47 changes: 47 additions & 0 deletions internal/printer/printer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package printer

import (
"fmt"
"time"
)

type UnsupportedPrinterError struct {
ptype string
}

func (e *UnsupportedPrinterError) Error() string {
return fmt.Sprintf("unsupported printer type %s", e.ptype)
}

func NewUnsupportedPrinterError(ptype string) error {
return &UnsupportedPrinterError{
ptype: ptype,
}
}

type Printer interface {
AddColumns(columns ...string)
AddField(string, ...FieldOption)
AddTimeField(now, t time.Time, c func(string) string)
EndRow()
Render() error
}

type FieldOption func(*tableField)

// WithTruncate overrides the truncation function for the field. The function should transform a string
// argument into a string that fits within the given display width. The default behavior is to truncate the
// value by adding "..." in the end. Pass nil to disable truncation for this value.
func WithTruncate(fn func(int, string) string) FieldOption {
return func(f *tableField) {
f.truncateFunc = fn
}
}

// WithColor sets the color function for the field. The function should transform a string value by wrapping
// it in ANSI escape codes. The color function will not be used if the table was initialized in non-terminal mode.
func WithColor(fn func(string) string) FieldOption {
return func(f *tableField) {
f.colorFunc = fn
}
}
37 changes: 7 additions & 30 deletions internal/tableprinter/table.go → internal/printer/table.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Package tableprinter facilitates rendering column-formatted data to a terminal and TSV-formatted data to
// TablePrinter facilitates rendering column-formatted data to a terminal and TSV-formatted data to
// a script or a file. It is suitable for presenting tabular data in a human-readable format that is
// guaranteed to fit within the given viewport, while at the same time offering the same data in a
// machine-readable format for scripts.
package tableprinter
package printer

import (
"fmt"
Expand All @@ -13,37 +13,14 @@ import (
"github.com/tmeckel/azdo-cli/internal/text"
)

type FieldOption func(*tableField)

type TablePrinter interface {
AddField(string, ...FieldOption)
AddTimeField(now, t time.Time, c func(string) string)
HeaderRow(columns ...string)
EndRow()
Render() error
}

// WithTruncate overrides the truncation function for the field. The function should transform a string
// argument into a string that fits within the given display width. The default behavior is to truncate the
// value by adding "..." in the end. Pass nil to disable truncation for this value.
func WithTruncate(fn func(int, string) string) FieldOption {
return func(f *tableField) {
f.truncateFunc = fn
}
}

// WithColor sets the color function for the field. The function should transform a string value by wrapping
// it in ANSI escape codes. The color function will not be used if the table was initialized in non-terminal mode.
func WithColor(fn func(string) string) FieldOption {
return func(f *tableField) {
f.colorFunc = fn
}
Printer
}

// New initializes a table printer with terminal mode and terminal width. When terminal mode is enabled, the
// NewTablePrinter initializes a table printer with terminal mode and terminal width. When terminal mode is enabled, the
// output will be human-readable, column-formatted to fit available width, and rendered with color support.
// In non-terminal mode, the output is tab-separated and all truncation of values is disabled.
func New(w io.Writer, isTTY bool, maxWidth int) (tp TablePrinter, err error) {
func NewTablePrinter(w io.Writer, isTTY bool, maxWidth int) (tp TablePrinter, err error) {
if isTTY {
tp = &ttyTablePrinter{
out: w,
Expand Down Expand Up @@ -71,7 +48,7 @@ type ttyTablePrinter struct {

var _ TablePrinter = &ttyTablePrinter{}

func (t *ttyTablePrinter) HeaderRow(columns ...string) {
func (t *ttyTablePrinter) AddColumns(columns ...string) {
for _, col := range columns {
t.AddField(strings.ToUpper(col))
}
Expand Down Expand Up @@ -249,7 +226,7 @@ func (t *tsvTablePrinter) AddField(text string, opts ...FieldOption) {
t.currentCol++
}

func (t *tsvTablePrinter) HeaderRow(columns ...string) {
func (t *tsvTablePrinter) AddColumns(columns ...string) {
}

func (t *tsvTablePrinter) EndRow() {
Expand Down