Skip to content

Commit

Permalink
feat: --include-reviews
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos Alexandro Becker <[email protected]>
  • Loading branch information
caarlos0 committed Jul 14, 2021
1 parent 65e0c17 commit d69f972
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 17 deletions.
36 changes: 28 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ func main() {
Usage: "Time to look back to gather info (0s means everything). Examples: e.g. 2y, 1mo, 1w, 10d, 20h, 15m, 25s, 10ms, etc. Note that GitHub data comes summarized by week, so this is not",
Value: "0s",
},
cli.BoolFlag{
Name: "include-reviews",
Usage: "Include PR reviews in the stats",
},
}
app.Action = func(c *cli.Context) error {
token := c.String("token")
Expand All @@ -68,6 +72,8 @@ func main() {
return cli.NewExitError("invalid --since duration", 1)
}

includeReviews := c.Bool("include-reviews")

userBlacklist, repoBlacklist := buildBlacklists(blacklist)

allStats, err := orgstats.Gather(
Expand All @@ -77,12 +83,14 @@ func main() {
repoBlacklist,
c.String("github-url"),
time.Now().UTC().Add(-1*time.Duration(since)),
includeReviews,
)
spin.Stop()
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
printHighlights(allStats, top)
fmt.Println()
printHighlights(allStats, top, includeReviews)
return nil
}
if err := app.Run(os.Args); err != nil {
Expand All @@ -106,15 +114,17 @@ func buildBlacklists(blacklist []string) ([]string, []string) {
return userBlacklist, repoBlacklist
}

func printHighlights(s orgstats.Stats, top int) {
data := []struct {
stats []orgstats.StatPair
trophy string
kind string
}{
type statUI struct {
stats []orgstats.StatPair
trophy string
kind string
}

func printHighlights(s orgstats.Stats, top int, includeReviews bool) {
data := []statUI{
{
stats: orgstats.Sort(s, orgstats.ExtractCommits),
trophy: "Commit",
trophy: "Commits",
kind: "commits",
}, {
stats: orgstats.Sort(s, orgstats.ExtractAdditions),
Expand All @@ -126,6 +136,16 @@ func printHighlights(s orgstats.Stats, top int) {
kind: "lines removed",
},
}

if includeReviews {
data = append(data, statUI{
stats: orgstats.Sort(s, orgstats.Reviews),
trophy: "Pull Requests Reviewed",
kind: "pull requests reviewed",
},
)
}

for _, d := range data {
fmt.Printf("\033[1m%s champions are:\033[0m\n", d.trophy)
j := top
Expand Down
5 changes: 5 additions & 0 deletions orgstats/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ var ExtractDeletions = func(st Stat) int {
return st.Deletions
}

// Reviews extract the reviewed prs section of the given stat
var Reviews = func(st Stat) int {
return st.Reviews
}

func Sort(s Stats, extract Extract) []StatPair {
var result []StatPair
for key, value := range s.data {
Expand Down
131 changes: 122 additions & 9 deletions orgstats/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package orgstats

import (
"context"
"fmt"
"log"
"strings"
"time"

Expand All @@ -10,7 +12,7 @@ import (

// Stat represents an user adds, rms and commits count
type Stat struct {
Additions, Deletions, Commits int
Additions, Deletions, Commits, Reviews int
}

// Stats contains the user->Stat mapping
Expand All @@ -28,17 +30,105 @@ func NewStats(since time.Time) Stats {
}

// Gather a given organization's stats
func Gather(token, org string, userBlacklist, repoBlacklist []string, url string, since time.Time) (Stats, error) {
func Gather(
token, org string,
userBlacklist, repoBlacklist []string,
url string,
since time.Time,
includeReviewStats bool,
) (Stats, error) {
ctx := context.Background()
allStats := NewStats(since)
client, err := newClient(ctx, token, url)
if err != nil {
return allStats, err
return Stats{}, err
}

allStats := NewStats(since)
if err := gatherLineStats(
ctx,
client,
org,
userBlacklist,
repoBlacklist,
&allStats,
); err != nil {
return Stats{}, err
}

if !includeReviewStats {
return allStats, nil
}

for user := range allStats.data {
if err := gatherReviewStats(
ctx,
client,
org,
user,
userBlacklist,
repoBlacklist,
&allStats,
since,
); err != nil {
return Stats{}, err
}
}

return allStats, nil
}

func gatherReviewStats(
ctx context.Context,
client *github.Client,
org, user string,
userBlacklist, repoBlacklist []string,
allStats *Stats,
since time.Time,
) error {
ts := since.Format("2006-01-02")
// review:approved, review:changes_requested
reviewed, err := search(ctx, client, fmt.Sprintf("user:%s is:pr reviewed-by:%s created:>%s", org, user, ts))
if err != nil {
return err
}
allStats.addReviewStats(user, reviewed)
return nil
}

func search(
ctx context.Context,
client *github.Client,
query string,
) (int, error) {
// log.Printf("searching '%s'", query)
result, _, err := client.Search.Issues(ctx, query, &github.SearchOptions{
ListOptions: github.ListOptions{
PerPage: 1,
},
})
if rateErr, ok := err.(*github.RateLimitError); ok {
handleRateLimit(rateErr)
return search(ctx, client, query)
}
if _, ok := err.(*github.AcceptedError); ok {
return search(ctx, client, query)
}
if err != nil {
return 0, fmt.Errorf("failed to search: %s: %w", query, err)
}
return *result.Total, nil
}

func gatherLineStats(
ctx context.Context,
client *github.Client,
org string,
userBlacklist, repoBlacklist []string,
allStats *Stats,
) error {
allRepos, err := repos(ctx, client, org)
if err != nil {
return allStats, err
return err
}

for _, repo := range allRepos {
Expand All @@ -47,7 +137,7 @@ func Gather(token, org string, userBlacklist, repoBlacklist []string, url string
}
stats, serr := getStats(ctx, client, org, *repo.Name)
if serr != nil {
return allStats, serr
return serr
}
for _, cs := range stats {
if isBlacklisted(userBlacklist, cs.Author.GetLogin()) {
Expand All @@ -56,7 +146,7 @@ func Gather(token, org string, userBlacklist, repoBlacklist []string, url string
allStats.add(cs)
}
}
return allStats, err
return err
}

func isBlacklisted(blacklist []string, s string) bool {
Expand All @@ -68,7 +158,13 @@ func isBlacklisted(blacklist []string, s string) bool {
return false
}

func (s Stats) add(cs *github.ContributorStats) {
func (s *Stats) addReviewStats(user string, reviewed int) {
stat := s.data[user]
stat.Reviews += reviewed
s.data[user] = stat
}

func (s *Stats) add(cs *github.ContributorStats) {
if cs.Author == nil {
return
}
Expand All @@ -87,6 +183,10 @@ func (s Stats) add(cs *github.ContributorStats) {
stat.Additions += adds
stat.Deletions += rms
stat.Commits += commits
if stat.Additions+stat.Deletions+stat.Commits == 0 && !s.since.IsZero() {
// ignore users with no activity when running with a since time
return
}
s.data[*cs.Author.Login] = stat
}

Expand All @@ -97,6 +197,10 @@ func repos(ctx context.Context, client *github.Client, org string) ([]*github.Re
var allRepos []*github.Repository
for {
repos, resp, err := client.Repositories.ListByOrg(ctx, org, opt)
if rateErr, ok := err.(*github.RateLimitError); ok {
handleRateLimit(rateErr)
continue
}
if err != nil {
return allRepos, err
}
Expand All @@ -113,7 +217,7 @@ func getStats(ctx context.Context, client *github.Client, org, repo string) ([]*
stats, _, err := client.Repositories.ListContributorsStats(ctx, org, repo)
if err != nil {
if rateErr, ok := err.(*github.RateLimitError); ok {
time.Sleep(time.Now().UTC().Sub(rateErr.Rate.Reset.Time.UTC()))
handleRateLimit(rateErr)
return getStats(ctx, client, org, repo)
}
if _, ok := err.(*github.AcceptedError); ok {
Expand All @@ -122,3 +226,12 @@ func getStats(ctx context.Context, client *github.Client, org, repo string) ([]*
}
return stats, err
}

func handleRateLimit(err *github.RateLimitError) {
s := err.Rate.Reset.UTC().Sub(time.Now().UTC())
if s < 0 {
s = 5 * time.Second
}
log.Printf("hit rate limit, waiting %v", s)
time.Sleep(s)
}

0 comments on commit d69f972

Please sign in to comment.