From bcddef0857f53bc7ba68b6331bdf70f09e45b7fb Mon Sep 17 00:00:00 2001 From: John McBride Date: Wed, 28 Aug 2024 19:31:36 -0600 Subject: [PATCH] feat: Refactors API client into hand rolled sdk in api/ directory Signed-off-by: John McBride --- api/client.go | 47 ++++++ api/services/contributors/contributors.go | 173 ++++++++++++++++++++++ api/services/contributors/spec.go | 18 +++ api/services/histogram/histogram.go | 44 ++++++ api/services/histogram/spec.go | 22 +++ api/services/repository/repository.go | 67 +++++++++ api/services/repository/spec.go | 102 +++++++++++++ api/services/spec.go | 10 ++ {pkg/api => api}/validation.go | 2 +- cmd/insights/contributors.go | 108 +++++++------- cmd/insights/repositories.go | 89 +++++------ cmd/insights/user-contributions.go | 33 ++--- cmd/insights/utils.go | 9 +- go.mod | 1 - go.sum | 2 - pkg/api/client.go | 34 ----- pkg/constants/flags.go | 2 +- 17 files changed, 591 insertions(+), 172 deletions(-) create mode 100644 api/client.go create mode 100644 api/services/contributors/contributors.go create mode 100644 api/services/contributors/spec.go create mode 100644 api/services/histogram/histogram.go create mode 100644 api/services/histogram/spec.go create mode 100644 api/services/repository/repository.go create mode 100644 api/services/repository/spec.go create mode 100644 api/services/spec.go rename {pkg/api => api}/validation.go (63%) delete mode 100644 pkg/api/client.go diff --git a/api/client.go b/api/client.go new file mode 100644 index 0000000..021661f --- /dev/null +++ b/api/client.go @@ -0,0 +1,47 @@ +package api + +import ( + "net/http" + + "github.com/open-sauced/pizza-cli/api/services/contributors" + "github.com/open-sauced/pizza-cli/api/services/histogram" + "github.com/open-sauced/pizza-cli/api/services/repository" +) + +type Client struct { + RepositoryService *repository.Service + ContributorService *contributors.Service + HistogramService *histogram.Service + + // The configured http client for making API requests + httpClient *http.Client + + // The API endpoint to use when making requests + // Example: https://api.opensauced.pizza + endpoint string +} + +func NewClient(endpoint string) *Client { + client := Client{ + httpClient: &http.Client{}, + endpoint: endpoint, + } + + repositoryService := repository.Service{ + Endpoint: client.endpoint, + } + + contributorService := contributors.Service{ + Endpoint: client.endpoint, + } + + histogramService := histogram.Service{ + Endpoint: client.endpoint, + } + + client.RepositoryService = &repositoryService + client.ContributorService = &contributorService + client.HistogramService = &histogramService + + return &client +} diff --git a/api/services/contributors/contributors.go b/api/services/contributors/contributors.go new file mode 100644 index 0000000..d4201a6 --- /dev/null +++ b/api/services/contributors/contributors.go @@ -0,0 +1,173 @@ +package contributors + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +type Service struct { + Endpoint string +} + +func (s *Service) NewPullRequestContributors(repos []string, rangeVal int) (*ContribResponse, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/contributors/insights/new", s.Endpoint) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + q.Set("repos", strings.Join(repos, ",")) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var newContributorsResponse ContribResponse + if err := json.NewDecoder(resp.Body).Decode(&newContributorsResponse); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return &newContributorsResponse, resp, nil +} + +func (s *Service) RecentPullRequestContributors(repos []string, rangeVal int) (*ContribResponse, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/contributors/insights/recent", s.Endpoint) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + q.Set("repos", strings.Join(repos, ",")) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var recentContributorsResponse ContribResponse + if err := json.NewDecoder(resp.Body).Decode(&recentContributorsResponse); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return &recentContributorsResponse, resp, nil +} + +func (s *Service) AlumniPullRequestContributors(repos []string, rangeVal int) (*ContribResponse, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/contributors/insights/alumni", s.Endpoint) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + q.Set("repos", strings.Join(repos, ",")) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var alumniContributorsResponse ContribResponse + if err := json.NewDecoder(resp.Body).Decode(&alumniContributorsResponse); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return &alumniContributorsResponse, resp, nil +} + +func (s *Service) RepeatPullRequestContributors(repos []string, rangeVal int) (*ContribResponse, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/contributors/insights/repeat", s.Endpoint) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + q.Set("repos", strings.Join(repos, ",")) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var repeatContributorsResponse ContribResponse + if err := json.NewDecoder(resp.Body).Decode(&repeatContributorsResponse); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return &repeatContributorsResponse, resp, nil +} + +func (s *Service) SearchPullRequestContributors(repoIDs []int, rangeVal int) (*ContribResponse, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/contributors/search", s.Endpoint) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + q.Set("repoIds", strings.Join(strings.Fields(fmt.Sprint(repoIDs)), ",")) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var searchContributorsResponse ContribResponse + if err := json.NewDecoder(resp.Body).Decode(&searchContributorsResponse); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return &searchContributorsResponse, resp, nil +} diff --git a/api/services/contributors/spec.go b/api/services/contributors/spec.go new file mode 100644 index 0000000..92552dc --- /dev/null +++ b/api/services/contributors/spec.go @@ -0,0 +1,18 @@ +package contributors + +import ( + "time" + + "github.com/open-sauced/pizza-cli/api/services" +) + +type DbContributor struct { + AuthorLogin string `json:"author_login"` + UserID int `json:"user_id"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ContribResponse struct { + Data []DbContributor `json:"data"` + Meta services.MetaData `json:"meta"` +} diff --git a/api/services/histogram/histogram.go b/api/services/histogram/histogram.go new file mode 100644 index 0000000..a9b00f2 --- /dev/null +++ b/api/services/histogram/histogram.go @@ -0,0 +1,44 @@ +package histogram + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +type Service struct { + Endpoint string +} + +func (s *Service) PrsHistogram(repo string, rangeVal int) ([]PrHistogramData, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/histogram/pull-requests", s.Endpoint) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + q.Set("repo", repo) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var prHistogramData []PrHistogramData + if err := json.NewDecoder(resp.Body).Decode(&prHistogramData); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return prHistogramData, resp, nil +} diff --git a/api/services/histogram/spec.go b/api/services/histogram/spec.go new file mode 100644 index 0000000..1600262 --- /dev/null +++ b/api/services/histogram/spec.go @@ -0,0 +1,22 @@ +package histogram + +import "time" + +type PrHistogramData struct { + Bucket time.Time `json:"bucket"` + PrCount int `json:"pr_count"` + AcceptedPrs int `json:"accepted_prs"` + OpenPrs int `json:"open_prs"` + ClosedPrs int `json:"closed_prs"` + DraftPrs int `json:"draft_prs"` + ActivePrs int `json:"active_prs"` + SpamPrs int `json:"spam_prs"` + PRVelocity int `json:"pr_velocity"` + CollaboratorAssociatedPrs int `json:"collaborator_associated_prs"` + ContributorAssociatedPrs int `json:"contributor_associated_prs"` + MemberAssociatedPrs int `json:"member_associated_prs"` + NonAssociatedPrs int `json:"non_associated_prs"` + OwnerAssociatedPrs int `json:"owner_associated_prs"` + CommentsOnPrs int `json:"comments_on_prs"` + ReviewCommentsOnPrs int `json:"review_comments_on_prs"` +} diff --git a/api/services/repository/repository.go b/api/services/repository/repository.go new file mode 100644 index 0000000..a2be3a6 --- /dev/null +++ b/api/services/repository/repository.go @@ -0,0 +1,67 @@ +package repository + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type Service struct { + Endpoint string +} + +func (rs *Service) FindOneByOwnerAndRepo(owner string, repo string) (*DbRepository, *http.Response, error) { + url := fmt.Sprintf("%s/v2/repos/%s/%s", rs.Endpoint, owner, repo) + + resp, err := http.Get(url) + if err != nil { + return nil, resp, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, resp, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + var repository DbRepository + if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil { + return nil, resp, fmt.Errorf("error decoding response: %v", err) + } + + return &repository, resp, nil +} + +func (rs *Service) FindContributorsByOwnerAndRepo(owner string, repo string, rangeVal int) (*ContributorsResponse, *http.Response, error) { + baseURL := fmt.Sprintf("%s/v2/repos/%s/%s", rs.Endpoint, owner, repo) + + // Create URL with query parameters + u, err := url.Parse(baseURL) + if err != nil { + return nil, nil, fmt.Errorf("error parsing URL: %v", err) + } + + q := u.Query() + q.Set("range", fmt.Sprintf("%d", rangeVal)) + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return nil, resp, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, resp, err + } + + var contributorsResp ContributorsResponse + err = json.Unmarshal(body, &contributorsResp) + if err != nil { + return nil, resp, err + } + + return &contributorsResp, resp, nil +} diff --git a/api/services/repository/spec.go b/api/services/repository/spec.go new file mode 100644 index 0000000..e22f654 --- /dev/null +++ b/api/services/repository/spec.go @@ -0,0 +1,102 @@ +package repository + +import "time" + +type DbRepository struct { + ID int `json:"id"` + UserID int `json:"user_id"` + Size int `json:"size"` + Issues int `json:"issues"` + Stars int `json:"stars"` + Forks int `json:"forks"` + Watchers int `json:"watchers"` + Subscribers int `json:"subscribers"` + Network int `json:"network"` + IsFork bool `json:"is_fork"` + IsPrivate bool `json:"is_private"` + IsTemplate bool `json:"is_template"` + IsArchived bool `json:"is_archived"` + IsDisabled bool `json:"is_disabled"` + HasIssues bool `json:"has_issues"` + HasProjects bool `json:"has_projects"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + HasDiscussions bool `json:"has_discussions"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt time.Time `json:"pushed_at"` + DefaultBranch string `json:"default_branch"` + NodeID string `json:"node_id"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SvnURL string `json:"svn_url"` + MirrorURL *string `json:"mirror_url"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + Language string `json:"language"` + License string `json:"license"` + URL string `json:"url"` + Homepage string `json:"homepage"` + Topics []string `json:"topics"` + OSSFScorecardTotalScore int `json:"ossf_scorecard_total_score"` + OSSFScorecardDependencyUpdateScore int `json:"ossf_scorecard_dependency_update_score"` + OSSFScorecardFuzzingScore int `json:"ossf_scorecard_fuzzing_score"` + OSSFScorecardMaintainedScore int `json:"ossf_scorecard_maintained_score"` + OSSFScorecardUpdatedAt time.Time `json:"ossf_scorecard_updated_at"` + OpenIssuesCount int `json:"open_issues_count"` + ClosedIssuesCount int `json:"closed_issues_count"` + IssuesVelocityCount int `json:"issues_velocity_count"` + OpenPRsCount int `json:"open_prs_count"` + ClosedPRsCount int `json:"closed_prs_count"` + MergedPRsCount int `json:"merged_prs_count"` + DraftPRsCount int `json:"draft_prs_count"` + SpamPRsCount int `json:"spam_prs_count"` + PRVelocityCount int `json:"pr_velocity_count"` + ForkVelocity int `json:"fork_velocity"` + PRActiveCount int `json:"pr_active_count"` + ActivityRatio float64 `json:"activity_ratio"` + ContributorConfidence float64 `json:"contributor_confidence"` + Health float64 `json:"health"` + LastPushedAt time.Time `json:"last_pushed_at"` + LastMainPushedAt time.Time `json:"last_main_pushed_at"` +} + +// DbContributorInfo represents the structure of a single contributor +type DbContributorInfo struct { + ID int `json:"id"` + Login string `json:"login"` + AvatarURL string `json:"avatar_url"` + Company string `json:"company"` + Location string `json:"location"` + OSCR float64 `json:"oscr"` + Repos []string `json:"repos"` + Tags []string `json:"tags"` + Commits int `json:"commits"` + PRsCreated int `json:"prs_created"` + PRsReviewed int `json:"prs_reviewed"` + IssuesCreated int `json:"issues_created"` + CommitComments int `json:"commit_comments"` + IssueComments int `json:"issue_comments"` + PRReviewComments int `json:"pr_review_comments"` + Comments int `json:"comments"` + TotalContributions int `json:"total_contributions"` + LastContributed time.Time `json:"last_contributed"` + DevstatsUpdatedAt time.Time `json:"devstats_updated_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ContributorsResponse represents the structure of the contributors endpoint response +type ContributorsResponse struct { + Data []DbContributorInfo `json:"data"` + Meta struct { + Page int `json:"page"` + Limit int `json:"limit"` + ItemCount int `json:"itemCount"` + PageCount int `json:"pageCount"` + HasPreviousPage bool `json:"hasPreviousPage"` + HasNextPage bool `json:"hasNextPage"` + } `json:"meta"` +} diff --git a/api/services/spec.go b/api/services/spec.go new file mode 100644 index 0000000..a9499bf --- /dev/null +++ b/api/services/spec.go @@ -0,0 +1,10 @@ +package services + +type MetaData struct { + Page int `json:"page"` + Limit int `json:"limit"` + ItemCount int `json:"itemCount"` + PageCount int `json:"pageCount"` + HasPreviousPage bool `json:"hasPreviousPage"` + HasNextPage bool `json:"hasNextPage"` +} diff --git a/pkg/api/validation.go b/api/validation.go similarity index 63% rename from pkg/api/validation.go rename to api/validation.go index 6236ac5..8937760 100644 --- a/pkg/api/validation.go +++ b/api/validation.go @@ -1,5 +1,5 @@ package api -func IsValidRange(period int32) bool { +func IsValidRange(period int) bool { return period == 7 || period == 30 || period == 90 } diff --git a/cmd/insights/contributors.go b/cmd/insights/contributors.go index cd320ef..36b2fde 100644 --- a/cmd/insights/contributors.go +++ b/cmd/insights/contributors.go @@ -2,7 +2,6 @@ package insights import ( "bytes" - "context" "encoding/csv" "errors" "fmt" @@ -11,17 +10,17 @@ import ( "sync" bubblesTable "github.com/charmbracelet/bubbles/table" - "github.com/open-sauced/go-api/client" "github.com/spf13/cobra" - "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/open-sauced/pizza-cli/api" + "github.com/open-sauced/pizza-cli/api/services/contributors" "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/open-sauced/pizza-cli/pkg/utils" ) type contributorsOptions struct { // APIClient is the http client for making calls to the open-sauced api - APIClient *client.APIClient + APIClient *api.Client // Repos is the array of git repository urls Repos []string @@ -29,8 +28,8 @@ type contributorsOptions struct { // 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 + // RangeVal is the number of days, used for query filtering + RangeVal int // Output is the formatting style for command output Output string @@ -53,20 +52,20 @@ func NewContributorsCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, _ []string) error { endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) - opts.APIClient = api.NewGoClient(endpointURL) + opts.APIClient = api.NewClient(endpointURL) output, _ := cmd.Flags().GetString(constants.FlagNameOutput) opts.Output = output - return opts.run(context.TODO()) + return opts.run() }, } 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 (7,30,90)") + cmd.Flags().IntVarP(&opts.RangeVal, constants.FlagNameRange, "r", 30, "Number of days to look-back (7,30,90)") return cmd } -func (opts *contributorsOptions) run(ctx context.Context) error { - if !api.IsValidRange(opts.Period) { - return fmt.Errorf("invalid period: %d, accepts (7,30,90)", opts.Period) +func (opts *contributorsOptions) run() error { + if !api.IsValidRange(opts.RangeVal) { + return fmt.Errorf("invalid period: %d, accepts (7,30,90)", opts.RangeVal) } repositories, err := utils.HandleRepositoryValues(opts.Repos, opts.FilePath) @@ -87,7 +86,7 @@ func (opts *contributorsOptions) run(ctx context.Context) error { waitGroup.Add(1) go func() { defer waitGroup.Done() - allData, err := findAllContributorsInsights(ctx, opts, repoURL) + allData, err := findAllContributorsInsights(opts, repoURL) if err != nil { errorChan <- err return @@ -210,26 +209,29 @@ func (cis contributorsInsightsSlice) OutputTable() (string, error) { return strings.Join(tables, separator), nil } -func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoURL string) (*contributorsInsights, error) { - repo, err := findRepositoryByOwnerAndRepoName(ctx, opts.APIClient, repoURL) +func findAllContributorsInsights(opts *contributorsOptions, repoURL string) (*contributorsInsights, error) { + var ( + waitGroup = new(sync.WaitGroup) + errorChan = make(chan error, 4) + ) + + repo, err := findRepositoryByOwnerAndRepoName(opts.APIClient, repoURL) if err != nil { return nil, fmt.Errorf("could not get contributors insights for repository %s: %w", repoURL, err) } if repo == nil { return nil, nil } + repoContributorsInsights := &contributorsInsights{ - RepoID: int(repo.Id), - RepoURL: repo.SvnUrl, + RepoID: 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 := findNewRepositoryContributors(ctx, opts.APIClient, repo.FullName, opts.Period) + response, err := findNewRepositoryContributors(opts.APIClient, repo.FullName, opts.RangeVal) if err != nil { errorChan <- err return @@ -238,10 +240,11 @@ func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoContributorsInsights.New = append(repoContributorsInsights.New, data.AuthorLogin) } }() + waitGroup.Add(1) go func() { defer waitGroup.Done() - response, err := findRecentRepositoryContributors(ctx, opts.APIClient, repo.FullName, opts.Period) + response, err := findRecentRepositoryContributors(opts.APIClient, repo.FullName, opts.RangeVal) if err != nil { errorChan <- err return @@ -250,10 +253,11 @@ func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoContributorsInsights.Recent = append(repoContributorsInsights.Recent, data.AuthorLogin) } }() + waitGroup.Add(1) go func() { defer waitGroup.Done() - response, err := findAlumniRepositoryContributors(ctx, opts.APIClient, repo.FullName, opts.Period) + response, err := findAlumniRepositoryContributors(opts.APIClient, repo.FullName, opts.RangeVal) if err != nil { errorChan <- err return @@ -262,10 +266,11 @@ func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoContributorsInsights.Alumni = append(repoContributorsInsights.Alumni, data.AuthorLogin) } }() + waitGroup.Add(1) go func() { defer waitGroup.Done() - response, err := findRepeatRepositoryContributors(ctx, opts.APIClient, repo.FullName, opts.Period) + response, err := findRepeatRepositoryContributors(opts.APIClient, repo.FullName, opts.RangeVal) if err != nil { errorChan <- err return @@ -274,6 +279,7 @@ func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, repoContributorsInsights.Repeat = append(repoContributorsInsights.Repeat, data.AuthorLogin) } }() + waitGroup.Wait() close(errorChan) if len(errorChan) > 0 { @@ -286,50 +292,38 @@ func findAllContributorsInsights(ctx context.Context, opts *contributorsOptions, return repoContributorsInsights, nil } -func findNewRepositoryContributors(ctx context.Context, apiClient *client.APIClient, repo string, period int32) (*client.SearchAllPullRequestContributors200Response, error) { - data, _, err := apiClient.ContributorsServiceAPI. - NewPullRequestContributors(ctx). - Repos(repo). - Range_(period). - Execute() +func findNewRepositoryContributors(apiClient *api.Client, repo string, period int) (*contributors.ContribResponse, error) { + response, _, err := apiClient.ContributorService.NewPullRequestContributors([]string{repo}, period) if err != nil { - return nil, fmt.Errorf("error while calling 'ContributorsServiceAPI.NewPullRequestContributors' with repository %s': %w", repo, err) + return nil, fmt.Errorf("error while calling 'ContributorsService.NewPullRequestContributors' with repository %s': %w", repo, err) } - return data, nil + + return response, nil } -func findRecentRepositoryContributors(ctx context.Context, apiClient *client.APIClient, repo string, period int32) (*client.SearchAllPullRequestContributors200Response, error) { - data, _, err := apiClient.ContributorsServiceAPI. - FindAllRecentPullRequestContributors(ctx). - Repos(repo). - Range_(period). - Execute() +func findRecentRepositoryContributors(apiClient *api.Client, repo string, period int) (*contributors.ContribResponse, error) { + response, _, err := apiClient.ContributorService.RecentPullRequestContributors([]string{repo}, period) if err != nil { - return nil, fmt.Errorf("error while calling 'ContributorsServiceAPI.FindAllRecentPullRequestContributors' with repository %s': %w", repo, err) + return nil, fmt.Errorf("error while calling 'ContributorsService.RecentPullRequestContributors' with repository %s': %w", repo, err) } - return data, nil + + return response, nil } -func findAlumniRepositoryContributors(ctx context.Context, apiClient *client.APIClient, repo string, period int32) (*client.SearchAllPullRequestContributors200Response, error) { - data, _, err := apiClient.ContributorsServiceAPI. - FindAllChurnPullRequestContributors(ctx). - Repos(repo). - Range_(period). - Execute() +func findAlumniRepositoryContributors(apiClient *api.Client, repo string, period int) (*contributors.ContribResponse, error) { + response, _, err := apiClient.ContributorService.AlumniPullRequestContributors([]string{repo}, period) if err != nil { - return nil, fmt.Errorf("error while calling 'ContributorsServiceAPI.FindAllChurnPullRequestContributors' with repository %s': %w", repo, err) + return nil, fmt.Errorf("error while calling 'ContributorsService.AlumniPullRequestContributors' with repository %s': %w", repo, err) } - return data, nil + + return response, nil } -func findRepeatRepositoryContributors(ctx context.Context, apiClient *client.APIClient, repo string, period int32) (*client.SearchAllPullRequestContributors200Response, error) { - data, _, err := apiClient.ContributorsServiceAPI. - FindAllRepeatPullRequestContributors(ctx). - Repos(repo). - Range_(period). - Execute() +func findRepeatRepositoryContributors(apiClient *api.Client, repo string, period int) (*contributors.ContribResponse, error) { + response, _, err := apiClient.ContributorService.RepeatPullRequestContributors([]string{repo}, period) if err != nil { - return nil, fmt.Errorf("error while calling 'ContributorsServiceAPI.FindAllRepeatPullRequestContributors' with repository %s: %w", repo, err) + return nil, fmt.Errorf("error while calling 'ContributorsService.RepeatPullRequestContributors' with repository %s': %w", repo, err) } - return data, nil + + return response, nil } diff --git a/cmd/insights/repositories.go b/cmd/insights/repositories.go index 3a55148..cbadef1 100644 --- a/cmd/insights/repositories.go +++ b/cmd/insights/repositories.go @@ -1,7 +1,6 @@ package insights import ( - "context" "errors" "fmt" "slices" @@ -10,17 +9,18 @@ import ( "sync" bubblesTable "github.com/charmbracelet/bubbles/table" - "github.com/open-sauced/go-api/client" "github.com/spf13/cobra" - "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/open-sauced/pizza-cli/api" + "github.com/open-sauced/pizza-cli/api/services/contributors" + "github.com/open-sauced/pizza-cli/api/services/histogram" "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/open-sauced/pizza-cli/pkg/utils" ) type repositoriesOptions struct { // APIClient is the http client for making calls to the open-sauced api - APIClient *client.APIClient + APIClient *api.Client // Repos is the array of git repository urls Repos []string @@ -28,9 +28,9 @@ type repositoriesOptions struct { // 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 + // RangeVal is the number of days, used for query filtering // Constrained to either 30 or 60 - Period int32 + RangeVal int // Output is the formatting style for command output Output string @@ -54,18 +54,18 @@ func NewRepositoriesCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, _ []string) error { endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) - opts.APIClient = api.NewGoClient(endpointURL) + opts.APIClient = api.NewClient(endpointURL) output, _ := cmd.Flags().GetString(constants.FlagNameOutput) opts.Output = output - return opts.run(context.TODO()) + return opts.run() }, } 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") + cmd.Flags().IntVarP(&opts.RangeVal, constants.FlagNameRange, "p", 30, "Number of days to look-back") return cmd } -func (opts *repositoriesOptions) run(ctx context.Context) error { +func (opts *repositoriesOptions) run() error { repositories, err := utils.HandleRepositoryValues(opts.Repos, opts.FilePath) if err != nil { return err @@ -84,7 +84,7 @@ func (opts *repositoriesOptions) run(ctx context.Context) error { waitGroup.Add(1) go func() { defer waitGroup.Done() - allData, err := findAllRepositoryInsights(ctx, opts, repoURL) + allData, err := findAllRepositoryInsights(opts, repoURL) if err != nil { errorChan <- err return @@ -179,8 +179,8 @@ func (ris repositoryInsightsSlice) OutputTable() (string, error) { return strings.Join(tables, separator), nil } -func findAllRepositoryInsights(ctx context.Context, opts *repositoriesOptions, repoURL string) (*repositoryInsights, error) { - repo, err := findRepositoryByOwnerAndRepoName(ctx, opts.APIClient, repoURL) +func findAllRepositoryInsights(opts *repositoriesOptions, repoURL string) (*repositoryInsights, error) { + repo, err := findRepositoryByOwnerAndRepoName(opts.APIClient, repoURL) if err != nil { return nil, fmt.Errorf("could not get repository insights for repository %s: %w", repoURL, err) } @@ -188,35 +188,38 @@ func findAllRepositoryInsights(ctx context.Context, opts *repositoriesOptions, r return nil, nil } repoInsights := &repositoryInsights{ - RepoID: int(repo.Id), - RepoURL: repo.SvnUrl, + RepoID: 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.FullName, opts.Period) + response, err := getPullRequestInsights(opts.APIClient, repo.FullName, opts.RangeVal) if err != nil { errorChan <- err return } - repoInsights.AllPullRequests = int(response.PrCount) - repoInsights.AcceptedPullRequests = int(response.AcceptedPrs) - repoInsights.SpamPullRequests = int(response.SpamPrs) + repoInsights.AllPullRequests = response.PrCount + repoInsights.AcceptedPullRequests = response.AcceptedPrs + repoInsights.SpamPullRequests = response.SpamPrs }() + waitGroup.Add(1) go func() { defer waitGroup.Done() - response, err := searchAllPullRequestContributors(ctx, opts.APIClient, repo.Id, opts.Period) + response, err := searchAllPullRequestContributors(opts.APIClient, []int{repo.ID}, opts.RangeVal) if err != nil { errorChan <- err return } var contributors []string - for _, contributor := range response { + for _, contributor := range response.Data { contributors = append(contributors, contributor.AuthorLogin) } repoInsights.Contributors = contributors @@ -230,47 +233,31 @@ func findAllRepositoryInsights(ctx context.Context, opts *repositoriesOptions, r } return nil, allErrors } + return repoInsights, nil } -func getPullRequestInsights(ctx context.Context, apiClient *client.APIClient, repo string, period int32) (*client.DbPullRequestGitHubEventsHistogram, error) { - data, _, err := apiClient.HistogramGenerationServiceAPI. - PrsHistogram(ctx). - Repo(repo). - Execute() +func getPullRequestInsights(apiClient *api.Client, repo string, rangeVal int) (*histogram.PrHistogramData, error) { + data, _, err := apiClient.HistogramService.PrsHistogram(repo, rangeVal) if err != nil { return nil, fmt.Errorf("error while calling 'PullRequestsServiceAPI.GetPullRequestInsights' with repository %s': %w", repo, err) } - index := slices.IndexFunc(data, func(prHisto client.DbPullRequestGitHubEventsHistogram) bool { - return int32(prHisto.Bucket.Unix()) == period + + index := slices.IndexFunc(data, func(prHisto histogram.PrHistogramData) bool { + return int(prHisto.Bucket.Unix()) == rangeVal }) if index == -1 { - return nil, fmt.Errorf("could not find pull request insights for repository %s with interval %d", repo, period) + return nil, fmt.Errorf("could not find pull request insights for repository %s with interval %d", repo, rangeVal) } + 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++ +func searchAllPullRequestContributors(apiClient *api.Client, repoIDs []int, rangeVal int) (*contributors.ContribResponse, error) { + data, _, err := apiClient.ContributorService.SearchPullRequestContributors(repoIDs, rangeVal) + if err != nil { + return nil, fmt.Errorf("error while calling 'ContributorService.SearchPullRequestContributors' with repository %v': %w", repoIDs, err) } - return allData, nil + + return data, nil } diff --git a/cmd/insights/user-contributions.go b/cmd/insights/user-contributions.go index eb6de93..e2f0a9d 100644 --- a/cmd/insights/user-contributions.go +++ b/cmd/insights/user-contributions.go @@ -2,7 +2,6 @@ package insights import ( "bytes" - "context" "encoding/csv" "errors" "fmt" @@ -11,17 +10,16 @@ import ( "sync" bubblesTable "github.com/charmbracelet/bubbles/table" - "github.com/open-sauced/go-api/client" "github.com/spf13/cobra" - "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/open-sauced/pizza-cli/api" "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/open-sauced/pizza-cli/pkg/utils" ) type userContributionsOptions struct { // APIClient is the http client for making calls to the open-sauced api - APIClient *client.APIClient + APIClient *api.Client // Repos is the array of git repository urls Repos []string @@ -63,22 +61,22 @@ func NewUserContributionsCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, _ []string) error { endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) - opts.APIClient = api.NewGoClient(endpointURL) + opts.APIClient = api.NewClient(endpointURL) output, _ := cmd.Flags().GetString(constants.FlagNameOutput) opts.Output = output - return opts.run(context.TODO()) + return opts.run() }, } 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") + cmd.Flags().Int32VarP(&opts.Period, constants.FlagNameRange, "p", 30, "Number of days, used for query filtering") cmd.Flags().StringSliceVarP(&opts.Users, "users", "u", []string{}, "Inclusive comma separated list of GitHub usernames to filter for") cmd.Flags().StringVarP(&opts.Sort, "sort", "s", "none", "Sort user contributions by (total, commits, prs)") return cmd } -func (opts *userContributionsOptions) run(ctx context.Context) error { +func (opts *userContributionsOptions) run() error { repositories, err := utils.HandleRepositoryValues(opts.Repos, opts.FilePath) if err != nil { return err @@ -107,7 +105,7 @@ func (opts *userContributionsOptions) run(ctx context.Context) error { go func() { defer waitGroup.Done() - data, err := findAllUserContributionsInsights(ctx, opts, repoURL) + data, err := findAllUserContributionsInsights(opts, repoURL) if err != nil { errorChan <- err return @@ -258,7 +256,7 @@ func (ucig userContributionsInsightGroup) OutputTable() (string, error) { return fmt.Sprintf("%s\n%s\n", ucig.RepoURL, utils.OutputTable(rows, columns)), nil } -func findAllUserContributionsInsights(ctx context.Context, opts *userContributionsOptions, repoURL string) (*userContributionsInsightGroup, error) { +func findAllUserContributionsInsights(opts *userContributionsOptions, repoURL string) (*userContributionsInsightGroup, error) { owner, name, err := utils.GetOwnerAndRepoFromURL(repoURL) if err != nil { return nil, err @@ -268,15 +266,10 @@ func findAllUserContributionsInsights(ctx context.Context, opts *userContributio RepoURL: repoURL, } - dataPoints, _, err := opts. - APIClient. - RepositoryServiceAPI. - FindContributorsByOwnerAndRepo(ctx, owner, name). - Range_(opts.Period). - Execute() + dataPoints, _, err := opts.APIClient.RepositoryService.FindContributorsByOwnerAndRepo(owner, name, 30) if err != nil { - return nil, fmt.Errorf("error while calling 'RepositoryServiceAPI.FindAllContributorsByRepoId' with repository %s/%s': %w", owner, name, err) + return nil, fmt.Errorf("error while calling API RepositoryService.FindContributorsByOwnerAndRepo with repository %s/%s': %w", owner, name, err) } for _, data := range dataPoints.Data { @@ -284,9 +277,9 @@ func findAllUserContributionsInsights(ctx context.Context, opts *userContributio if len(opts.usersMap) == 0 || ok { repoUserContributionsInsightGroup.Insights = append(repoUserContributionsInsightGroup.Insights, userContributionsInsights{ Login: data.Login, - Commits: int(data.Commits), - PrsCreated: int(data.PrsCreated), - TotalContributions: int(data.Commits) + int(data.PrsCreated), + Commits: data.Commits, + PrsCreated: data.PRsCreated, + TotalContributions: data.Commits + data.PRsCreated, }) } } diff --git a/cmd/insights/utils.go b/cmd/insights/utils.go index a7bca74..d81b02d 100644 --- a/cmd/insights/utils.go +++ b/cmd/insights/utils.go @@ -1,24 +1,23 @@ package insights import ( - "context" "fmt" "net/http" - "github.com/open-sauced/go-api/client" - + "github.com/open-sauced/pizza-cli/api" + "github.com/open-sauced/pizza-cli/api/services/repository" "github.com/open-sauced/pizza-cli/pkg/utils" ) // findRepositoryByOwnerAndRepoName returns an API client Db Repo // based on the given repository URL -func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.APIClient, repoURL string) (*client.DbRepo, error) { +func findRepositoryByOwnerAndRepoName(apiClient *api.Client, repoURL string) (*repository.DbRepository, 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() + repo, response, err := apiClient.RepositoryService.FindOneByOwnerAndRepo(owner, repoName) 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) diff --git a/go.mod b/go.mod index 971cc6b..1c90786 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/cli/browser v1.3.0 github.com/go-git/go-git/v5 v5.12.0 github.com/jpmcb/gopherlogs v0.2.0 - github.com/open-sauced/go-api/client v0.0.0-20240205155059-a3159bc0517e github.com/posthog/posthog-go v1.2.19 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 6f949cf..3adc853 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,6 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/open-sauced/go-api/client v0.0.0-20240205155059-a3159bc0517e h1:3j5r7ArokAO+u8vhgQPklp5qnGxA+MkXluRYt2qTnik= -github.com/open-sauced/go-api/client v0.0.0-20240205155059-a3159bc0517e/go.mod h1:W/TRuLUqYpMvkmElDUQvQ07xlxhK8TOfpwRh8SCAuNA= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/pkg/api/client.go b/pkg/api/client.go deleted file mode 100644 index b2bdfd6..0000000 --- a/pkg/api/client.go +++ /dev/null @@ -1,34 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/open-sauced/go-api/client" -) - -type Client struct { - // The configured http client for making API requests - HTTPClient *http.Client - - // The API endpoint to use when making requests - // Example: https://api.opensauced.pizza - Endpoint string -} - -// NewClient creates a new OpenSauced API client for making http requests -func NewClient(endpoint string) *Client { - return &Client{ - HTTPClient: &http.Client{}, - Endpoint: endpoint, - } -} - -func NewGoClient(endpoint string) *client.APIClient { - configuration := client.NewConfiguration() - configuration.Servers = client.ServerConfigurations{ - { - URL: endpoint, - }, - } - return client.NewAPIClient(configuration) -} diff --git a/pkg/constants/flags.go b/pkg/constants/flags.go index 7489e8b..304aa6e 100644 --- a/pkg/constants/flags.go +++ b/pkg/constants/flags.go @@ -5,7 +5,7 @@ const ( FlagNameEndpoint = "endpoint" FlagNameFile = "file" FlagNameOutput = "output" - FlagNamePeriod = "period" + FlagNameRange = "range" FlagNameTelemetry = "disable-telemetry" FlagNameWait = "wait" )