From dc148d6fe17b9aa96ad6951aefd0a7fd7cf0e160 Mon Sep 17 00:00:00 2001 From: Tsvetoslav Dimov <32335835+cecobask@users.noreply.github.com> Date: Tue, 3 Oct 2023 01:51:07 +0200 Subject: [PATCH] feat: provide repository insights (#38) --- .github/workflows/release.yaml | 2 +- .github/workflows/test.yaml | 8 +- Makefile | 2 +- README.md | 2 + cmd/insights/contributors.go | 177 +++++++++++++-------- cmd/insights/insights.go | 1 + cmd/insights/repositories.go | 274 +++++++++++++++++++++++++++++++++ cmd/root/root.go | 3 +- go.mod | 12 +- go.sum | 21 +-- pkg/constants/flags.go | 1 + pkg/constants/output.go | 7 + pkg/utils/output.go | 52 +++++++ 13 files changed, 474 insertions(+), 88 deletions(-) create mode 100644 cmd/insights/repositories.go create mode 100644 pkg/constants/output.go create mode 100644 pkg/utils/output.go diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ea68252..4543068 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,7 +60,7 @@ jobs: - name: Set up Go uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 with: - go-version: 1.21 + go-version: 1.21.x - name: Check out code uses: actions/checkout@v3 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0a9c29e..9b7fcbe 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,11 +23,11 @@ jobs: - name: Set up Go uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 with: - go-version: 1.20.x + go-version: 1.21.x - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.53 + version: v1.54.2 test: runs-on: ubuntu-latest @@ -36,7 +36,7 @@ jobs: - name: Set up Go uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 with: - go-version: 1.20.x + go-version: 1.21.x - name: Test run: make test @@ -47,6 +47,6 @@ jobs: - name: Set up Go uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 with: - go-version: 1.20.x + go-version: 1.21.x - name: Build go binary run: make build diff --git a/Makefile b/Makefile index 0db730e..c1da7b6 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ lint: --rm \ -v ./:/app \ -w /app \ - golangci/golangci-lint:v1.53.3 \ + golangci/golangci-lint:v1.54.2 \ golangci-lint run -v test: diff --git a/README.md b/README.md index 6acaeb1..f217849 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Available Commands: insights Gather insights about git contributors, repositories, users and pull requests login Log into the CLI application via GitHub repo-query Ask questions about a GitHub repository + show Get visual metrics of a repository version Displays the build version of the CLI Flags: @@ -44,6 +45,7 @@ Flags: --disable-telemetry Disable sending telemetry data to OpenSauced -e, --endpoint string The API endpoint to send requests to (default "https://api.opensauced.pizza") -h, --help help for pizza + -o, --output string The formatting style for command output (default "table") Use "pizza [command] --help" for more information about a command. ``` diff --git a/cmd/insights/contributors.go b/cmd/insights/contributors.go index cb87ad7..1420f6a 100644 --- a/cmd/insights/contributors.go +++ b/cmd/insights/contributors.go @@ -6,10 +6,10 @@ import ( "fmt" "net/http" "strconv" + "strings" "sync" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/lipgloss" + bubblesTable "github.com/charmbracelet/bubbles/table" "github.com/open-sauced/go-api/client" "github.com/open-sauced/pizza-cli/pkg/api" "github.com/open-sauced/pizza-cli/pkg/constants" @@ -17,7 +17,7 @@ import ( "github.com/spf13/cobra" ) -type Options struct { +type contributorsOptions struct { // APIClient is the http client for making calls to the open-sauced api APIClient *client.APIClient @@ -29,13 +29,16 @@ type Options struct { // Period is the number of days, used for query filtering Period int32 + + // Output is the formatting style for command output + Output string } // NewContributorsCommand returns a new cobra command for 'pizza insights contributors' func NewContributorsCommand() *cobra.Command { - opts := &Options{} + opts := &contributorsOptions{} cmd := &cobra.Command{ - Use: "contributors [flags]", + Use: "contributors url... [flags]", Short: "Gather insights about contributors of indexed git repositories", Long: "Gather insights about contributors of indexed git repositories. This command will show new, recent, alumni, repeat contributors for each git repository", Args: func(cmd *cobra.Command, args []string) error { @@ -49,7 +52,9 @@ func NewContributorsCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) opts.APIClient = api.NewGoClient(endpointURL) - return run(opts) + output, _ := cmd.Flags().GetString(constants.FlagNameOutput) + opts.Output = output + return opts.run(context.TODO()) }, } cmd.Flags().StringVarP(&opts.FilePath, constants.FlagNameFile, "f", "", "Path to yaml file containing an array of git repository urls") @@ -57,76 +62,118 @@ func NewContributorsCommand() *cobra.Command { return cmd } -func run(opts *Options) error { +func (opts *contributorsOptions) run(ctx context.Context) error { repositories, err := utils.HandleRepositoryValues(opts.Repos, opts.FilePath) if err != nil { return err } var ( - waitGroup = new(sync.WaitGroup) - errorChan = make(chan error, len(repositories)) + waitGroup = new(sync.WaitGroup) + errorChan = make(chan error, len(repositories)) + insightsChan = make(chan contributorsInsights, len(repositories)) + doneChan = make(chan struct{}) + insights = make(contributorsInsightsSlice, 0, len(repositories)) + allErrors error ) - for url := range repositories { - repoURL := url - waitGroup.Add(1) - go func() { - defer waitGroup.Done() - repoContributorsInsights, err := findAllRepositoryContributorsInsights(context.TODO(), opts, repoURL) + go func() { + for url := range repositories { + repoURL := url + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + allData, err := findAllContributorsInsights(ctx, opts, repoURL) + if err != nil { + errorChan <- err + return + } + if allData == nil { + return + } + insightsChan <- *allData + }() + } + waitGroup.Wait() + close(doneChan) + }() + for { + select { + case err = <-errorChan: + allErrors = errors.Join(allErrors, err) + case data := <-insightsChan: + insights = append(insights, data) + case <-doneChan: + if allErrors != nil { + return allErrors + } + output, err := insights.BuildOutput(opts.Output) if err != nil { - errorChan <- err - return + return err } - repoContributorsInsights.RenderTable() - }() - } - waitGroup.Wait() - close(errorChan) - var allErrors error - for err = range errorChan { - allErrors = errors.Join(allErrors, err) + fmt.Println(output) + return nil + } } - return allErrors } -type repositoryContributorsInsights struct { - RepoID int32 - RepoURL string - New []string - Recent []string - Alumni []string - Repeat []string +type contributorsInsights struct { + RepoURL string `json:"repo_url" yaml:"repo_url"` + RepoID int `json:"-" yaml:"-"` + New []string `json:"new" yaml:"new"` + Recent []string `json:"recent" yaml:"recent"` + Alumni []string `json:"alumni" yaml:"alumni"` + Repeat []string `json:"repeat" yaml:"repeat"` } -func (rci *repositoryContributorsInsights) RenderTable() { - if rci == nil { - return - } - rows := []table.Row{ - {"New contributors", strconv.Itoa(len(rci.New))}, - {"Recent contributors", strconv.Itoa(len(rci.Recent))}, - {"Alumni contributors", strconv.Itoa(len(rci.Alumni))}, - {"Repeat contributors", strconv.Itoa(len(rci.Repeat))}, +type contributorsInsightsSlice []contributorsInsights + +func (cis contributorsInsightsSlice) BuildOutput(format string) (string, error) { + switch format { + case constants.OutputTable: + return cis.OutputTable() + case constants.OutputJSON: + return utils.OutputJSON(cis) + case constants.OutputYAML: + return utils.OutputYAML(cis) + default: + return "", fmt.Errorf("unknown output format %s", format) } - columns := []table.Column{ - { - Title: "Repository URL", - Width: 20, - }, - { - Title: rci.RepoURL, - Width: len(rci.RepoURL), - }, +} + +func (cis contributorsInsightsSlice) OutputTable() (string, error) { + tables := make([]string, 0, len(cis)) + for i := range cis { + rows := []bubblesTable.Row{ + { + "New contributors", + strconv.Itoa(len(cis[i].New)), + }, + { + "Recent contributors", + strconv.Itoa(len(cis[i].Recent)), + }, + { + "Alumni contributors", + strconv.Itoa(len(cis[i].Alumni)), + }, + { + "Repeat contributors", + strconv.Itoa(len(cis[i].Repeat)), + }, + } + columns := []bubblesTable.Column{ + { + Title: "Repository URL", + Width: utils.GetMaxTableRowWidth(rows), + }, + { + Title: cis[i].RepoURL, + Width: len(cis[i].RepoURL), + }, + } + tables = append(tables, utils.OutputTable(rows, columns)) } - styles := table.DefaultStyles() - styles.Header.MarginTop(1) - styles.Selected = lipgloss.NewStyle() - repoTable := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithHeight(len(rows)), - table.WithStyles(styles), - ) - fmt.Println(repoTable.View()) + separator := fmt.Sprintf("\n%s\n", strings.Repeat("―", 3)) + return strings.Join(tables, separator), nil } func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.APIClient, repoURL string) (*client.DbRepo, error) { @@ -146,7 +193,7 @@ func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.API return repo, nil } -func findAllRepositoryContributorsInsights(ctx context.Context, opts *Options, repoURL string) (*repositoryContributorsInsights, error) { +func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoURL string) (*contributorsInsights, error) { repo, err := findRepositoryByOwnerAndRepoName(ctx, opts.APIClient, repoURL) if err != nil { return nil, fmt.Errorf("could not get contributors insights for repository %s: %w", repoURL, err) @@ -154,9 +201,9 @@ func findAllRepositoryContributorsInsights(ctx context.Context, opts *Options, r if repo == nil { return nil, nil } - repoContributorsInsights := &repositoryContributorsInsights{ - RepoID: repo.Id, - RepoURL: repoURL, + repoContributorsInsights := &contributorsInsights{ + RepoID: int(repo.Id), + RepoURL: repo.SvnUrl, } var ( waitGroup = new(sync.WaitGroup) diff --git a/cmd/insights/insights.go b/cmd/insights/insights.go index 2ab442f..e09539d 100644 --- a/cmd/insights/insights.go +++ b/cmd/insights/insights.go @@ -15,5 +15,6 @@ func NewInsightsCommand() *cobra.Command { }, } cmd.AddCommand(NewContributorsCommand()) + cmd.AddCommand(NewRepositoriesCommand()) return cmd } diff --git a/cmd/insights/repositories.go b/cmd/insights/repositories.go new file mode 100644 index 0000000..51d3b64 --- /dev/null +++ b/cmd/insights/repositories.go @@ -0,0 +1,274 @@ +package insights + +import ( + "context" + "errors" + "fmt" + "slices" + "strconv" + "strings" + "sync" + + bubblesTable "github.com/charmbracelet/bubbles/table" + "github.com/open-sauced/go-api/client" + "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/open-sauced/pizza-cli/pkg/constants" + "github.com/open-sauced/pizza-cli/pkg/utils" + "github.com/spf13/cobra" +) + +type repositoriesOptions struct { + // APIClient is the http client for making calls to the open-sauced api + APIClient *client.APIClient + + // Repos is the array of git repository urls + Repos []string + + // FilePath is the path to yaml file containing an array of git repository urls + FilePath string + + // Period is the number of days, used for query filtering + // Constrained to either 30 or 60 + Period int32 + + // Output is the formatting style for command output + Output string +} + +// NewRepositoriesCommand returns a new cobra command for 'pizza insights repositories' +func NewRepositoriesCommand() *cobra.Command { + opts := &repositoriesOptions{} + cmd := &cobra.Command{ + Use: "repositories url... [flags]", + Short: "Gather insights about indexed git repositories", + Long: "Gather insights about indexed git repositories. This command will show info about contributors, pull requests, etc.", + Args: func(cmd *cobra.Command, args []string) error { + fileFlag := cmd.Flags().Lookup(constants.FlagNameFile) + if !fileFlag.Changed && len(args) == 0 { + return fmt.Errorf("must specify git repository url argument(s) or provide %s flag", fileFlag.Name) + } + opts.Repos = append(opts.Repos, args...) + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) + opts.APIClient = api.NewGoClient(endpointURL) + output, _ := cmd.Flags().GetString(constants.FlagNameOutput) + opts.Output = output + return opts.run(context.TODO()) + }, + } + cmd.Flags().StringVarP(&opts.FilePath, constants.FlagNameFile, "f", "", "Path to yaml file containing an array of git repository urls") + cmd.Flags().Int32VarP(&opts.Period, constants.FlagNamePeriod, "p", 30, "Number of days, used for query filtering") + return cmd +} + +func (opts *repositoriesOptions) run(ctx context.Context) error { + repositories, err := utils.HandleRepositoryValues(opts.Repos, opts.FilePath) + if err != nil { + return err + } + var ( + waitGroup = new(sync.WaitGroup) + errorChan = make(chan error, len(repositories)) + insightsChan = make(chan repositoryInsights, len(repositories)) + doneChan = make(chan struct{}) + insights = make(repositoryInsightsSlice, 0, len(repositories)) + allErrors error + ) + go func() { + for url := range repositories { + repoURL := url + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + allData, err := findAllRepositoryInsights(ctx, opts, repoURL) + if err != nil { + errorChan <- err + return + } + if allData == nil { + return + } + insightsChan <- *allData + }() + } + waitGroup.Wait() + close(doneChan) + }() + for { + select { + case err = <-errorChan: + allErrors = errors.Join(allErrors, err) + case data := <-insightsChan: + insights = append(insights, data) + case <-doneChan: + if allErrors != nil { + return allErrors + } + output, err := insights.BuildOutput(opts.Output) + if err != nil { + return err + } + fmt.Println(output) + return nil + } + } +} + +type repositoryInsights struct { + RepoURL string `json:"repo_url" yaml:"repo_url"` + RepoID int `json:"-" yaml:"-"` + AllPullRequests int `json:"all_pull_requests" yaml:"all_pull_requests"` + AcceptedPullRequests int `json:"accepted_pull_requests" yaml:"accepted_pull_requests"` + SpamPullRequests int `json:"spam_pull_requests" yaml:"spam_pull_requests"` + Contributors []string `json:"contributors" yaml:"contributors"` +} + +type repositoryInsightsSlice []repositoryInsights + +func (ris repositoryInsightsSlice) BuildOutput(format string) (string, error) { + switch format { + case constants.OutputTable: + return ris.OutputTable() + case constants.OutputJSON: + return utils.OutputJSON(ris) + case constants.OutputYAML: + return utils.OutputYAML(ris) + default: + return "", fmt.Errorf("unknown output format %s", format) + } +} + +func (ris repositoryInsightsSlice) OutputTable() (string, error) { + tables := make([]string, 0, len(ris)) + for i := range ris { + rows := []bubblesTable.Row{ + { + "All pull requests", + strconv.Itoa(ris[i].AllPullRequests), + }, + { + "Accepted pull requests", + strconv.Itoa(ris[i].AcceptedPullRequests), + }, + { + "Spam pull requests", + strconv.Itoa(ris[i].SpamPullRequests), + }, + { + "Contributors", + strconv.Itoa(len(ris[i].Contributors)), + }, + } + columns := []bubblesTable.Column{ + { + Title: "Repository URL", + Width: utils.GetMaxTableRowWidth(rows), + }, + { + Title: ris[i].RepoURL, + Width: len(ris[i].RepoURL), + }, + } + tables = append(tables, utils.OutputTable(rows, columns)) + } + separator := fmt.Sprintf("\n%s\n", strings.Repeat("―", 3)) + return strings.Join(tables, separator), nil +} + +func findAllRepositoryInsights(ctx context.Context, opts *repositoriesOptions, repoURL string) (*repositoryInsights, error) { + repo, err := findRepositoryByOwnerAndRepoName(ctx, opts.APIClient, repoURL) + if err != nil { + return nil, fmt.Errorf("could not get repository insights for repository %s: %w", repoURL, err) + } + if repo == nil { + return nil, nil + } + repoInsights := &repositoryInsights{ + RepoID: int(repo.Id), + RepoURL: repo.SvnUrl, + } + var ( + waitGroup = new(sync.WaitGroup) + errorChan = make(chan error, 4) + ) + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + response, err := getPullRequestInsights(ctx, opts.APIClient, repo.Id, opts.Period) + if err != nil { + errorChan <- err + return + } + repoInsights.AllPullRequests = int(response.AllPrs) + repoInsights.AcceptedPullRequests = int(response.AcceptedPrs) + repoInsights.SpamPullRequests = int(response.SpamPrs) + }() + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + response, err := searchAllPullRequestContributors(ctx, opts.APIClient, repo.Id, opts.Period) + if err != nil { + errorChan <- err + return + } + var contributors []string + for _, contributor := range response { + contributors = append(contributors, contributor.AuthorLogin) + } + repoInsights.Contributors = contributors + }() + waitGroup.Wait() + close(errorChan) + if len(errorChan) > 0 { + var allErrors error + for err = range errorChan { + allErrors = errors.Join(allErrors, err) + } + return nil, allErrors + } + return repoInsights, nil +} + +func getPullRequestInsights(ctx context.Context, apiClient *client.APIClient, repoID, period int32) (*client.DbPRInsight, error) { + data, _, err := apiClient.PullRequestsServiceAPI. + GetPullRequestInsights(ctx). + RepoIds(strconv.Itoa(int(repoID))). + Execute() + if err != nil { + return nil, fmt.Errorf("error while calling 'PullRequestsServiceAPI.GetPullRequestInsights' with repository %d': %w", repoID, err) + } + index := slices.IndexFunc(data, func(insight client.DbPRInsight) bool { + return insight.Interval == period + }) + if index == -1 { + return nil, fmt.Errorf("could not find pull request insights for repository %d with interval %d", repoID, period) + } + return &data[index], nil +} + +func searchAllPullRequestContributors(ctx context.Context, apiClient *client.APIClient, repoID, period int32) ([]client.DbPullRequestContributor, error) { + var ( + allData []client.DbPullRequestContributor + page int32 = 1 + ) + for { + data, _, err := apiClient.ContributorsServiceAPI. + SearchAllPullRequestContributors(ctx). + RepoIds(strconv.Itoa(int(repoID))). + Range_(period). + Limit(50). + Page(page). + Execute() + if err != nil { + return nil, fmt.Errorf("error while calling 'ContributorsServiceAPI.SearchAllPullRequestContributors' with repository %d': %w", repoID, err) + } + allData = append(allData, data.Data...) + if !data.Meta.HasNextPage { + break + } + page++ + } + return allData, nil +} diff --git a/cmd/root/root.go b/cmd/root/root.go index ab86928..cf21f0e 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -19,7 +19,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd := &cobra.Command{ Use: "pizza [flags]", Short: "OpenSauced CLI", - Long: `A command line utility for insights, metrics, and all things OpenSauced`, + Long: "A command line utility for insights, metrics, and all things OpenSauced", RunE: run, Args: func(cmd *cobra.Command, args []string) error { betaFlag := cmd.Flags().Lookup(constants.FlagNameBeta) @@ -36,6 +36,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd.PersistentFlags().StringP(constants.FlagNameEndpoint, "e", constants.EndpointProd, "The API endpoint to send requests to") cmd.PersistentFlags().Bool(constants.FlagNameBeta, false, fmt.Sprintf("Shorthand for using the beta OpenSauced API endpoint (\"%s\"). Supersedes the '--%s' flag", constants.EndpointBeta, constants.FlagNameEndpoint)) cmd.PersistentFlags().Bool(constants.FlagNameTelemetry, false, "Disable sending telemetry data to OpenSauced") + cmd.PersistentFlags().StringP(constants.FlagNameOutput, "o", constants.OutputTable, "The formatting style for command output") cmd.AddCommand(bake.NewBakeCommand()) cmd.AddCommand(repoquery.NewRepoQueryCommand()) diff --git a/go.mod b/go.mod index c589872..e09d154 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ module github.com/open-sauced/pizza-cli -go 1.20 +go 1.21 require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.7.1 + github.com/charmbracelet/lipgloss v0.8.0 github.com/cli/browser v1.2.0 - github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff + github.com/open-sauced/go-api/client v0.0.0-20230925192938-8a8b1fa31f60 github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - golang.org/x/term v0.9.0 + golang.org/x/term v0.12.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -28,10 +28,10 @@ require ( github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.9.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index eadba7c..fa4ea40 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5 github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= +github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= @@ -20,6 +20,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -35,10 +36,10 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= -github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff h1:sHfgCRtAg3xzNXoqqJ2MhDLwPj4GhC99ksDYC+K1BtM= -github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff/go.mod h1:W/TRuLUqYpMvkmElDUQvQ07xlxhK8TOfpwRh8SCAuNA= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/open-sauced/go-api/client v0.0.0-20230925192938-8a8b1fa31f60 h1:JqzkafXPIqfn88GkVuVkN3HSWRvBzYLpn7LyVZq+fSg= +github.com/open-sauced/go-api/client v0.0.0-20230925192938-8a8b1fa31f60/go.mod h1:W/TRuLUqYpMvkmElDUQvQ07xlxhK8TOfpwRh8SCAuNA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69 h1:01dHVodha5BzrMtVmcpPeA4VYbZEsTXQ6m4123zQXJk= github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69/go.mod h1:migYMxlAqcnQy+3eN8mcL0b2tpKy6R+8Zc0lxwk4dKM= @@ -60,10 +61,10 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/pkg/constants/flags.go b/pkg/constants/flags.go index 6feef7b..7489e8b 100644 --- a/pkg/constants/flags.go +++ b/pkg/constants/flags.go @@ -4,6 +4,7 @@ const ( FlagNameBeta = "beta" FlagNameEndpoint = "endpoint" FlagNameFile = "file" + FlagNameOutput = "output" FlagNamePeriod = "period" FlagNameTelemetry = "disable-telemetry" FlagNameWait = "wait" diff --git a/pkg/constants/output.go b/pkg/constants/output.go new file mode 100644 index 0000000..3cdb360 --- /dev/null +++ b/pkg/constants/output.go @@ -0,0 +1,7 @@ +package constants + +const ( + OutputJSON = "json" + OutputTable = "table" + OutputYAML = "yaml" +) diff --git a/pkg/utils/output.go b/pkg/utils/output.go new file mode 100644 index 0000000..dae1842 --- /dev/null +++ b/pkg/utils/output.go @@ -0,0 +1,52 @@ +package utils + +import ( + "encoding/json" + "strings" + + bubblesTable "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/lipgloss" + "gopkg.in/yaml.v3" +) + +func OutputJSON(entity interface{}) (string, error) { + output, err := json.MarshalIndent(entity, "", " ") + if err != nil { + return "", err + } + return string(output), nil +} + +func OutputYAML(entity interface{}) (string, error) { + output, err := yaml.Marshal(entity) + if err != nil { + return "", err + } + return strings.TrimSuffix(string(output), "\n"), nil +} + +func OutputTable(rows []bubblesTable.Row, columns []bubblesTable.Column) string { + styles := bubblesTable.Styles{ + Cell: lipgloss.NewStyle().PaddingRight(1), + Header: lipgloss.NewStyle().Bold(true).PaddingRight(1), + Selected: lipgloss.NewStyle(), + } + table := bubblesTable.New( + bubblesTable.WithRows(rows), + bubblesTable.WithColumns(columns), + bubblesTable.WithHeight(len(rows)), + bubblesTable.WithStyles(styles), + ) + return table.View() +} + +func GetMaxTableRowWidth(rows []bubblesTable.Row) int { + var maxRowWidth int + for i := range rows { + rowWidth := len(rows[i][0]) + if rowWidth > maxRowWidth { + maxRowWidth = rowWidth + } + } + return maxRowWidth +}