Skip to content

Commit

Permalink
wip: Add "pizza insights user-contributions" command
Browse files Browse the repository at this point in the history
Signed-off-by: John McBride <[email protected]>
  • Loading branch information
jpmcb committed Oct 26, 2023
1 parent e97e256 commit 2dc38fb
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 52 deletions.
16 changes: 0 additions & 16 deletions cmd/insights/contributors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/csv"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -206,21 +205,6 @@ func (cis contributorsInsightsSlice) OutputTable() (string, error) {
return strings.Join(tables, separator), nil
}

func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.APIClient, repoURL string) (*client.DbRepo, error) {
owner, repoName, err := utils.GetOwnerAndRepoFromURL(repoURL)
if err != nil {
return nil, fmt.Errorf("could not extract owner and repo from url: %w", err)
}
repo, response, err := apiClient.RepositoryServiceAPI.FindOneByOwnerAndRepo(ctx, owner, repoName).Execute()
if err != nil {
if response != nil && response.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("repository %s is either non-existent, private, or has not been indexed yet", repoURL)
}
return nil, fmt.Errorf("error while calling 'RepositoryServiceAPI.FindOneByOwnerAndRepo' with owner %q and repo %q: %w", owner, repoName, err)
}
return repo, nil
}

func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoURL string) (*contributorsInsights, error) {
repo, err := findRepositoryByOwnerAndRepoName(ctx, opts.APIClient, repoURL)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/insights/insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ func NewInsightsCommand() *cobra.Command {
}
cmd.AddCommand(NewContributorsCommand())
cmd.AddCommand(NewRepositoriesCommand())
cmd.AddCommand(NewUserContributionsCommand())
return cmd
}
246 changes: 246 additions & 0 deletions cmd/insights/user-contributions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package insights

import (
"bytes"
"context"
"encoding/csv"
"errors"
"fmt"
"strconv"
"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 userContributionsOptions 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
Period int32

// Output is the formatting style for command output
Output string
}

// NewUserContributionsCommand
func NewUserContributionsCommand() *cobra.Command {
opts := &userContributionsOptions{}
cmd := &cobra.Command{
Use: "user-contributions url... [flags]",
Short: "",
Long: "",
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 *userContributionsOptions) 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 *userContributionsInsightGroup, len(repositories))
doneChan = make(chan struct{})
insights = make([]*userContributionsInsightGroup, 0, len(repositories))
allErrors error
)

go func() {
for url := range repositories {
repoURL := url
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
data, err := findAllUserContributionsInsights(ctx, opts, repoURL)
if err != nil {
errorChan <- err
return
}
if data == nil {
return
}
insightsChan <- data
}()
}

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
}
for _, insight := range insights {
output, err := insight.BuildOutput(opts.Output)
if err != nil {
return err
}
fmt.Println(output)
}

return nil
}
}
}

type userContributionsInsights struct {
Login string `json:"login" yaml:"login"`
Commits int `json:"commits" yaml:"commits"`
PrsCreated int `json:"prs_created" yaml:"prs_created"`
TotalContributions int `json:"total_contributions" yaml:"total_contributions"`
}

type userContributionsInsightGroup struct {
RepoURL string `json:"repo_url" yaml:"repo_url"`
Insights []userContributionsInsights
}

func (ucis userContributionsInsightGroup) BuildOutput(format string) (string, error) {
switch format {
case constants.OutputTable:
return ucis.OutputTable()
case constants.OutputJSON:
return utils.OutputJSON(ucis)
case constants.OutputYAML:
return utils.OutputYAML(ucis)
case constants.OuputCSV:
return ucis.OutputCSV()
default:
return "", fmt.Errorf("unknown output format %s", format)
}
}

func (ucis userContributionsInsightGroup) OutputCSV() (string, error) {
if len(ucis.Insights) == 0 {
return "", fmt.Errorf("repository is either non-existent or has not been indexed yet")
}
b := new(bytes.Buffer)
writer := csv.NewWriter(b)

// write headers
err := writer.WriteAll([][]string{
{ucis.RepoURL},
{"User", "Total", "Commits", "PRs Created"},
})
if err != nil {
return "", err
}

// write records
for _, uci := range ucis.Insights {
err := writer.WriteAll([][]string{
{
uci.Login,
strconv.Itoa(uci.Commits + uci.PrsCreated),
strconv.Itoa(uci.Commits),
strconv.Itoa(uci.PrsCreated),
},
})

if err != nil {
return "", err
}
}

return b.String(), nil
}

func (ucis userContributionsInsightGroup) OutputTable() (string, error) {
rows := []bubblesTable.Row{}

for _, uci := range ucis.Insights {
rows = append(rows, bubblesTable.Row{
uci.Login,
strconv.Itoa(uci.TotalContributions),
strconv.Itoa(uci.Commits),
strconv.Itoa(uci.PrsCreated),
})
}

columns := []bubblesTable.Column{
{
Title: "User",
Width: utils.GetMaxTableRowWidth(rows),
},
{
Title: "Total",
Width: 10,
},
{
Title: "Commits",
Width: 10,
},
{
Title: "PRs Created",
Width: 15,
},
}

return fmt.Sprintf("%s\n%s\n", ucis.RepoURL, utils.OutputTable(rows, columns)), nil
}

func findAllUserContributionsInsights(ctx context.Context, opts *userContributionsOptions, repoURL string) (*userContributionsInsightGroup, error) {
owner, name, err := utils.GetOwnerAndRepoFromURL(repoURL)
if err != nil {
return nil, err
}

repoUserContributionsInsightGroup := &userContributionsInsightGroup{
RepoURL: repoURL,
}

dataPoints, _, err := opts.APIClient.RepositoryServiceAPI.FindAllContributorsByRepoId(ctx, owner, name).Range_(opts.Period).Execute()
if err != nil {
return nil, fmt.Errorf("error while calling 'RepositoryServiceAPI.FindAllContributorsByRepoId' with repository %s/%s': %w", owner, name, err)
}

for _, data := range dataPoints {
repoUserContributionsInsightGroup.Insights = append(repoUserContributionsInsightGroup.Insights, userContributionsInsights{
Login: *data.Login,
Commits: int(data.Commits),
PrsCreated: int(data.PrsCreated),
TotalContributions: int(data.Commits) + int(data.PrsCreated),
})
}

return repoUserContributionsInsightGroup, nil
}
25 changes: 25 additions & 0 deletions cmd/insights/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package insights

import (
"context"
"fmt"
"net/http"

"github.com/open-sauced/go-api/client"
"github.com/open-sauced/pizza-cli/pkg/utils"
)

func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.APIClient, repoURL string) (*client.DbRepo, error) {
owner, repoName, err := utils.GetOwnerAndRepoFromURL(repoURL)
if err != nil {
return nil, fmt.Errorf("could not extract owner and repo from url: %w", err)
}
repo, response, err := apiClient.RepositoryServiceAPI.FindOneByOwnerAndRepo(ctx, owner, repoName).Execute()
if err != nil {
if response != nil && response.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("repository %s is either non-existent, private, or has not been indexed yet", repoURL)
}
return nil, fmt.Errorf("error while calling 'RepositoryServiceAPI.FindOneByOwnerAndRepo' with owner %q and repo %q: %w", owner, repoName, err)
}
return repo, nil
}
26 changes: 13 additions & 13 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@ go 1.21
require (
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.8.0
github.com/cli/browser v1.2.0
github.com/open-sauced/go-api/client v0.0.0-20230925192938-8a8b1fa31f60
github.com/charmbracelet/lipgloss v0.9.1
github.com/cli/browser v1.3.0
github.com/open-sauced/go-api/client v0.0.0-20231025234817-a8f01f3b26d8
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.12.0
golang.org/x/term v0.13.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
)
Loading

0 comments on commit 2dc38fb

Please sign in to comment.