From efdd4be5636dbae69fa7ef286de93331f85fc8d1 Mon Sep 17 00:00:00 2001 From: Tsvetoslav Dimov Date: Fri, 15 Sep 2023 15:43:43 +0100 Subject: [PATCH] feat: provide repository insights --- .github/workflows/release.yaml | 2 +- .github/workflows/test.yaml | 8 +- Makefile | 2 +- cmd/insights/contributors.go | 57 ++++++++---- cmd/insights/insights.go | 1 + cmd/insights/repo.go | 155 +++++++++++++++++++++++++++++++++ go.mod | 4 +- go.sum | 4 +- 8 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 cmd/insights/repo.go diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e994b1f..fee94b2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -59,7 +59,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/cmd/insights/contributors.go b/cmd/insights/contributors.go index cb87ad7..e698f96 100644 --- a/cmd/insights/contributors.go +++ b/cmd/insights/contributors.go @@ -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 @@ -33,9 +33,9 @@ type Options struct { // 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 +49,7 @@ 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) + 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,7 +57,7 @@ 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 @@ -71,7 +71,7 @@ func run(opts *Options) error { waitGroup.Add(1) go func() { defer waitGroup.Done() - repoContributorsInsights, err := findAllRepositoryContributorsInsights(context.TODO(), opts, repoURL) + repoContributorsInsights, err := findAllRepositoryContributorsInsights(ctx, opts, repoURL) if err != nil { errorChan <- err return @@ -89,7 +89,7 @@ func run(opts *Options) error { } type repositoryContributorsInsights struct { - RepoID int32 + RepoID int RepoURL string New []string Recent []string @@ -102,15 +102,31 @@ func (rci *repositoryContributorsInsights) RenderTable() { 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))}, + { + "Repository ID", + strconv.Itoa(len(strconv.Itoa(rci.RepoID))), + }, + { + "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)), + }, } columns := []table.Column{ { Title: "Repository URL", - Width: 20, + Width: getMaxTableRowWidth(rows), }, { Title: rci.RepoURL, @@ -129,6 +145,17 @@ func (rci *repositoryContributorsInsights) RenderTable() { fmt.Println(repoTable.View()) } +func getMaxTableRowWidth(rows []table.Row) int { + var maxRowWidth int + for i := range rows { + rowWidth := len(rows[i][0]) + if rowWidth > maxRowWidth { + maxRowWidth = rowWidth + } + } + return maxRowWidth +} + func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.APIClient, repoURL string) (*client.DbRepo, error) { owner, repoName, err := utils.GetOwnerAndRepoFromURL(repoURL) if err != nil { @@ -146,7 +173,7 @@ func findRepositoryByOwnerAndRepoName(ctx context.Context, apiClient *client.API return repo, nil } -func findAllRepositoryContributorsInsights(ctx context.Context, opts *Options, repoURL string) (*repositoryContributorsInsights, error) { +func findAllRepositoryContributorsInsights(ctx context.Context, opts *contributorsOptions, repoURL string) (*repositoryContributorsInsights, 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) @@ -155,8 +182,8 @@ func findAllRepositoryContributorsInsights(ctx context.Context, opts *Options, r return nil, nil } repoContributorsInsights := &repositoryContributorsInsights{ - RepoID: repo.Id, - RepoURL: repoURL, + 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..95a748a 100644 --- a/cmd/insights/insights.go +++ b/cmd/insights/insights.go @@ -15,5 +15,6 @@ func NewInsightsCommand() *cobra.Command { }, } cmd.AddCommand(NewContributorsCommand()) + cmd.AddCommand(NewRepoCommand()) return cmd } diff --git a/cmd/insights/repo.go b/cmd/insights/repo.go new file mode 100644 index 0000000..694bfb3 --- /dev/null +++ b/cmd/insights/repo.go @@ -0,0 +1,155 @@ +package insights + +import ( + "context" + "fmt" + "slices" + "strconv" + + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/lipgloss" + "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/spf13/cobra" +) + +type repoOptions struct { + // APIClient is the http client for making calls to the open-sauced api + APIClient *client.APIClient + + // Repo is a git repository url + Repo string + + // Period is the number of days, used for query filtering + // Constrained to either 30 or 60 + Period int32 +} + +// NewRepoCommand returns a new cobra command for 'pizza insights repo' +func NewRepoCommand() *cobra.Command { + opts := &repoOptions{} + cmd := &cobra.Command{ + Use: "repo url [flags]", + Short: "Gather insights about an indexed git repository", + Long: "Gather insights about an indexed git repository. This command will show info about contributors, pull requests, etc.", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("must provide exactly one git repository url") + } + opts.Repo = args[0] + period, _ := cmd.Flags().GetInt32(constants.FlagNamePeriod) + allowedPeriods := []int32{30, 60} + if !slices.Contains(allowedPeriods, period) { + return fmt.Errorf("%s flag must be one of %v", constants.FlagNamePeriod, allowedPeriods) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) + opts.APIClient = api.NewGoClient(endpointURL) + return opts.run(context.TODO()) + }, + } + cmd.Flags().Int32VarP(&opts.Period, constants.FlagNamePeriod, "p", 30, "Number of days, used for query filtering") + return cmd +} + +type repositoryInsights struct { + RepoID int `json:"repo_id"` + RepoURL string `json:"repo_url"` + AllPullRequests int `json:"all_pull_requests"` + AcceptedPullRequests int `json:"accepted_pull_requests"` + SpamPullRequests int `json:"spam_pull_requests"` +} + +func (opts *repoOptions) run(ctx context.Context) error { + repo, err := findRepositoryByOwnerAndRepoName(ctx, opts.APIClient, opts.Repo) + if err != nil { + return err + } + if repo == nil { + return nil + } + repoInsights := &repositoryInsights{ + RepoID: int(repo.Id), + RepoURL: repo.SvnUrl, + } + pullRequestInsights, err := getPullRequestInsights(ctx, opts.APIClient, repo.Id, opts.Period) + if err != nil { + return err + } + repoInsights.AllPullRequests = int(pullRequestInsights.AllPrs) + repoInsights.AcceptedPullRequests = int(pullRequestInsights.AcceptedPrs) + repoInsights.SpamPullRequests = int(pullRequestInsights.SpamPrs) + repoInsights.RenderTable() + return nil +} + +func (ri *repositoryInsights) RenderTable() { + if ri == nil { + return + } + rows := []table.Row{ + { + "Repository ID", + strconv.Itoa(ri.RepoID), + }, + { + "All pull requests", + strconv.Itoa(ri.AllPullRequests), + }, + { + "Accepted pull requests", + strconv.Itoa(ri.AcceptedPullRequests), + }, + { + "Spam pull requests", + strconv.Itoa(ri.SpamPullRequests), + }, + } + var maxRowWidth int + for i := range rows { + rowWidth := len(rows[i][0]) + if rowWidth > maxRowWidth { + maxRowWidth = rowWidth + } + } + columns := []table.Column{ + { + Title: "Repository URL", + Width: getMaxTableRowWidth(rows), + }, + { + Title: ri.RepoURL, + Width: len(ri.RepoURL), + }, + } + 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()) +} + +func getPullRequestInsights(ctx context.Context, apiClient *client.APIClient, repoID, period int32) (*client.DbPRInsight, error) { + data, _, err := apiClient.PullRequestsServiceAPI. + GetPullRequestInsights(ctx). + RepoIds(fmt.Sprintf("%d", 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 +} diff --git a/go.mod b/go.mod index d536864..8b396f1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/open-sauced/pizza-cli -go 1.20 +go 1.21 require ( github.com/charmbracelet/bubbles v0.16.1 @@ -33,3 +33,5 @@ require ( golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.3.8 // indirect ) + +replace github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff => github.com/cecobask/go-api/client v0.0.0-20230915140043-f201131f42d3 diff --git a/go.sum b/go.sum index faaf92a..16325ab 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/cecobask/go-api/client v0.0.0-20230915140043-f201131f42d3 h1:4iysAcbyHA8SrOcVLjO0E769e//mDkKEnM2MMq0D26Y= +github.com/cecobask/go-api/client v0.0.0-20230915140043-f201131f42d3/go.mod h1:W/TRuLUqYpMvkmElDUQvQ07xlxhK8TOfpwRh8SCAuNA= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.1 h1:LpdYfnu+Qc6XtvMz6d/6rRY71yttHTP5HtrjMgWvixc= @@ -34,8 +36,6 @@ 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/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=