-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: Add "pizza insights user-contributions" command
Signed-off-by: John McBride <[email protected]>
- Loading branch information
Showing
6 changed files
with
315 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.