From dee6c6ef17d888311177ebbbfdbcaa1fa91a96d6 Mon Sep 17 00:00:00 2001 From: Brian Downs Date: Mon, 13 Jan 2025 09:53:53 -0700 Subject: [PATCH] move stats logic from command to library and update output options (#527) --- cmd/release/cmd/stats.go | 136 +++++++-------------------------------- release/release.go | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 113 deletions(-) diff --git a/cmd/release/cmd/stats.go b/cmd/release/cmd/stats.go index bd4a923a..507cc0ef 100644 --- a/cmd/release/cmd/stats.go +++ b/cmd/release/cmd/stats.go @@ -2,23 +2,24 @@ package cmd import ( "context" + "encoding/json" "errors" "fmt" "os" - "sort" - "strings" "time" "github.com/briandowns/spinner" - "github.com/google/go-github/v39/github" + "github.com/rancher/ecm-distro-tools/release" "github.com/rancher/ecm-distro-tools/repository" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" ) var ( repo *string startDate *string endDate *string + format *string ) var repoToOwner = map[string]string{ @@ -27,17 +28,6 @@ var repoToOwner = map[string]string{ "k3s": "k3s-io", } -type monthly struct { - count int - captains []string - tags []string -} - -type relStats struct { - count int - monthly map[time.Month]monthly -} - // statsCmd represents the stats command var statsCmd = &cobra.Command{ Use: "stats", @@ -58,128 +48,48 @@ var statsCmd = &cobra.Command{ return errors.New("end date before start date") } - githubToken := os.Getenv("GITHUB_TOKEN") - ctx := context.Background() - client := repository.NewGithub(ctx, githubToken) + client := repository.NewGithub(ctx, rootConfig.Auth.GithubToken) s := spinner.New(spinner.CharSets[31], 100*time.Millisecond) s.HideCursor = true + s.Writer = os.Stderr s.Start() - var total int - data := make(map[int]relStats) - captains := make(map[string]int) + sd, err := release.Stats(ctx, client, from, to, repoToOwner[*repo], *repo) + if err != nil { + return err + } - lo := github.ListOptions{ - PerPage: 100, + var b []byte + + switch *format { + case "json": + b, err = json.Marshal(sd) + case "yaml": + b, err = yaml.Marshal(sd) + default: + return errors.New("unrecognized format") } - for { - releases, resp, err := client.Repositories.ListReleases(ctx, repoToOwner[*repo], *repo, &lo) - if err != nil { - return err - } - - for _, release := range releases { - releaseDate := release.GetCreatedAt().Time - if releaseDate.After(from) && (releaseDate.Before(to) || releaseDate.Equal(to)) { - total++ - - if _, ok := data[int(release.CreatedAt.Year())]; !ok { - data[int(release.CreatedAt.Year())] = relStats{ - count: 1, - monthly: map[time.Month]monthly{ - release.CreatedAt.Month(): { - count: 1, - captains: []string{ - *release.Author.Login, - }, - tags: []string{ - *release.Name, - }, - }, - }, - } - continue - } - - rs := data[int(release.CreatedAt.Year())] - rs.count++ - - mon := rs.monthly[release.CreatedAt.Month()] - mon.count++ - mon.captains = append(mon.captains, *release.Author.Login) - mon.tags = append(mon.tags, *release.Name) - - rs.monthly[release.CreatedAt.Month()] = mon - - data[int(release.CreatedAt.Year())] = rs - - if release.Author.Login != nil { - if _, ok := captains[*release.Author.Login]; !ok { - captains[*release.Author.Login]++ - continue - } - captains[*release.Author.Login]++ - } - } - } - - if resp.NextPage == 0 { - break - } - lo.Page = resp.NextPage + if err != nil { + return err } s.Stop() - for year := range data { - fmt.Printf("\n%d:\n", year) - - months := make([]int, 0, len(data[year].monthly)) - for k := range data[year].monthly { - months = append(months, int(k)) - } - sort.Ints(months) - - for _, m := range months { - mon := time.Month(m) - tmp := data[year].monthly[mon] - tmp.captains = dedup(tmp.captains) - data[year].monthly[mon] = tmp - captains := strings.Join(data[year].monthly[mon].captains, ", ") - tags := strings.Join(data[year].monthly[mon].tags, ", ") - fmt.Printf(" %-9s\n Count: %3d\n Captains: %s\n Tags: %s\n", - mon, data[year].monthly[mon].count, captains, tags) - } - } - - fmt.Printf("\nTotal: %d\n", total) + fmt.Println(string(b)) return nil }, } -func dedup(slice []string) []string { - seen := make(map[string]struct{}) - result := []string{} - - for _, val := range slice { - if _, ok := seen[val]; !ok { - seen[val] = struct{}{} - result = append(result, val) - } - } - - return result -} - func init() { rootCmd.AddCommand(statsCmd) repo = statsCmd.Flags().StringP("repo", "r", "", "repository") startDate = statsCmd.Flags().StringP("start", "s", "", "start date") endDate = statsCmd.Flags().StringP("end", "e", "", "end date") + format = statsCmd.Flags().StringP("format", "f", "json", "format (json|yaml)") if err := statsCmd.MarkFlagRequired("repo"); err != nil { fmt.Println(err.Error()) diff --git a/release/release.go b/release/release.go index ef927841..6c289a5c 100644 --- a/release/release.go +++ b/release/release.go @@ -762,6 +762,132 @@ func LatestPreRelease(ctx context.Context, client *github.Client, owner, repo, v return latestRelease(versions), nil } +// StatsMonthly +type StatsMonthly struct { + Count int + Captains []string + Tags []string +} + +// RelStats +type RelStats struct { + Count int + Monthly map[time.Month]StatsMonthly +} + +// StatsData +type StatsData struct { + Total int64 `json:"total"` + Data map[int]RelStats `json:"data"` + Captains map[string]int `json:"captains"` +} + +// dedup creates and returns a new slices based on the given slice +// but with duplicate entries removed. +func dedup(slice []string) []string { + seen := make(map[string]struct{}) + result := []string{} + + for _, val := range slice { + if _, ok := seen[val]; !ok { + seen[val] = struct{}{} + result = append(result, val) + } + } + + return result +} + +// Stats collects and processes information regarding a set of releases for the given repo +// over the given period of time. +func Stats(ctx context.Context, client *github.Client, startDate, endDate time.Time, owner, repo string) (*StatsData, error) { + if endDate.Before(startDate) { + return nil, errors.New("end date before start date") + } + + sd := StatsData{ + Data: make(map[int]RelStats), + Captains: make(map[string]int), + } + + lo := github.ListOptions{ + PerPage: 100, + } + for { + releases, resp, err := client.Repositories.ListReleases(ctx, owner, repo, &lo) + if err != nil { + return nil, err + } + + for _, release := range releases { + releaseDate := release.GetCreatedAt().Time + if releaseDate.After(startDate) && (releaseDate.Before(endDate) || releaseDate.Equal(endDate)) { + sd.Total++ + + if _, ok := sd.Data[int(release.CreatedAt.Year())]; !ok { + sd.Data[int(release.CreatedAt.Year())] = RelStats{ + Count: 1, + Monthly: map[time.Month]StatsMonthly{ + release.CreatedAt.Month(): { + Count: 1, + Captains: []string{ + *release.Author.Login, + }, + Tags: []string{ + *release.Name, + }, + }, + }, + } + continue + } + + rs := sd.Data[int(release.CreatedAt.Year())] + rs.Count++ + + mon := rs.Monthly[release.CreatedAt.Month()] + mon.Count++ + mon.Captains = append(mon.Captains, *release.Author.Login) + mon.Tags = append(mon.Tags, *release.Name) + + rs.Monthly[release.CreatedAt.Month()] = mon + + sd.Data[int(release.CreatedAt.Year())] = rs + + if release.Author.Login != nil { + if _, ok := sd.Captains[*release.Author.Login]; !ok { + sd.Captains[*release.Author.Login]++ + continue + } + sd.Captains[*release.Author.Login]++ + } + } + } + + if resp.NextPage == 0 { + break + } + lo.Page = resp.NextPage + } + + for year := range sd.Data { + months := make([]int, 0, len(sd.Data[year].Monthly)) + for k := range sd.Data[year].Monthly { + months = append(months, int(k)) + } + sort.Ints(months) + + for _, m := range months { + mon := time.Month(m) + tmp := sd.Data[year].Monthly[mon] + tmp.Captains = dedup(tmp.Captains) + sd.Data[year].Monthly[mon] = tmp + } + } + + return &sd, nil +} + func latestRelease(versions []*github.RepositoryRelease) *string { sort.Slice(versions, func(i, j int) bool { return versions[i].PublishedAt.Before(versions[j].PublishedAt.Time)