blob: a791056de4a6b147cb9dfb8578ab3dabbd4da025 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"github.com/google/go-github/github"
"golang.org/x/build/gerrit"
"golang.org/x/build/internal/gophers"
"golang.org/x/build/maintner"
"golang.org/x/build/maintner/godata"
"golang.org/x/oauth2"
)
var (
mode = flag.String("mode", "", "mode to run in. Valid values:\n\n"+modeSummary())
startTime = newTimeFlag("from", "1900-01-01", "start of time range for the 'range-stats' mode")
endTime = newTimeFlag("to", "2100-01-01", "end of time range for the 'range-stats' mode")
timeZone = flag.String("tz", "US/Pacific", "timezone to use for time values")
gerritProjects = newStringSetFlag("projects", "", "set of Gerrit projects to include; empty means all")
)
type stringSetFlag map[string]bool
func newStringSetFlag(name string, defVal string, desc string) *stringSetFlag {
var s stringSetFlag
s.Set(defVal)
flag.Var(&s, name, desc)
return &s
}
func (s *stringSetFlag) Includes(p string) bool {
if len(*s) == 0 {
return true
}
return (*s)[p]
}
func (s *stringSetFlag) Set(v string) error {
if v == "" {
*s = nil
return nil
}
elms := strings.Split(v, ",")
*s = make(map[string]bool, len(elms))
for _, e := range elms {
(*s)[e] = true
}
return nil
}
func (s *stringSetFlag) String() string {
var elms []string
for e := range *s {
elms = append(elms, e)
}
sort.Strings(elms)
return strings.Join(elms, ",")
}
func newTimeFlag(name, defVal, desc string) *time.Time {
var t time.Time
tf := (*timeFlag)(&t)
if err := tf.Set(defVal); err != nil {
panic(err.Error())
}
flag.Var(tf, name, desc)
return &t
}
type timeFlag time.Time
func (t *timeFlag) String() string { return time.Time(*t).String() }
func (t *timeFlag) Set(v string) error {
loc, err := time.LoadLocation(*timeZone)
if err != nil {
return err
}
for _, pat := range []string{
time.RFC3339Nano,
time.RFC3339,
"2006-01-02T15:04:05",
"2006-01-02T15:04",
"2006-01-02",
} {
parsedTime, err := time.ParseInLocation(pat, v, loc)
if err == nil {
*t = timeFlag(parsedTime)
return nil
}
}
return fmt.Errorf("unrecognized RFC3339 or prefix %q", v)
}
type handler struct {
fn func(*statsClient)
desc string
}
var modes = map[string]handler{
"find-github-email": {(*statsClient).findGithubEmails, "discover mappings between github usernames and emails"},
"gerrit-groups": {(*statsClient).gerritGroups, "print stats on gerrit groups"},
"github-groups": {(*statsClient).githubGroups, "print stats on github groups"},
"github-issue-close": {(*statsClient).githubIssueCloseStats, "print stats on github issues closes by quarter (googler-vs-not, unique numbers)"},
"gerrit-cls": {(*statsClient).gerritCLStats, "print stats on opened gerrit CLs by quarter"},
"workshop-stats": {(*statsClient).workshopStats, "print stats from contributor workshop"},
"find-gerrit-gophers": {(*statsClient).findGerritGophers, "discover mappings between internal/gopher entries and Gerrit IDs"},
"range-stats": {(*statsClient).rangeStats, "show various summaries of activity in the flag-provided time range"},
}
func modeSummary() string {
var buf bytes.Buffer
var sorted []string
for mode := range modes {
sorted = append(sorted, mode)
}
sort.Strings(sorted)
for _, mode := range sorted {
fmt.Fprintf(&buf, "%q: %s\n", mode, modes[mode].desc)
}
return buf.String()
}
type statsClient struct {
lazyGitHub *github.Client
lazyGerrit *gerrit.Client
corpusCache *maintner.Corpus
}
func (sc *statsClient) github() *github.Client {
if sc.lazyGitHub != nil {
return sc.lazyGitHub
}
ghc, err := getGithubClient()
if err != nil {
log.Fatal(err)
}
sc.lazyGitHub = ghc
return ghc
}
func (sc *statsClient) gerrit() *gerrit.Client {
if sc.lazyGerrit != nil {
return sc.lazyGerrit
}
gerrc := gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookieFileAuth(filepath.Join(os.Getenv("HOME"), ".gitcookies")))
sc.lazyGerrit = gerrc
return gerrc
}
func (sc *statsClient) corpus() *maintner.Corpus {
if sc.corpusCache == nil {
var err error
sc.corpusCache, err = godata.Get(context.Background())
if err != nil {
log.Fatalf("Loading maintner corpus: %v", err)
}
}
return sc.corpusCache
}
func main() {
flag.Parse()
if *mode == "" {
fmt.Fprintf(os.Stderr, "Missing required --mode flag.\n")
flag.Usage()
os.Exit(1)
}
h, ok := modes[*mode]
if !ok {
fmt.Fprintf(os.Stderr, "Unknown --mode flag.\n")
flag.Usage()
os.Exit(1)
}
sc := &statsClient{}
h.fn(sc)
}
func (sc *statsClient) gerritGroups() {
ctx := context.Background()
gerrc := sc.gerrit()
groups, err := gerrc.GetGroups(ctx)
if err != nil {
log.Fatalf("Gerrit.GetGroups: %v", err)
}
for name, gi := range groups {
switch name {
case "admins", "approvers", "may-start-trybots", "gophers",
"may-abandon-changes",
"may-forge-author-identity", "osp-team",
"release-managers":
members, err := gerrc.GetGroupMembers(ctx, gi.ID)
if err != nil {
log.Fatal(err)
}
numGoog, numExt := 0, 0
for _, member := range members {
//fmt.Printf(" %s: %+v\n", name, member)
p := gophers.GetGerritPerson(member)
if p == nil {
fmt.Printf("addPerson(%q, %q)\n", member.Name, member.Email)
} else {
if p.Googler {
numGoog++
} else {
numExt++
}
}
}
fmt.Printf("Group %s: %d total (%d googlers, %d external)\n", name, numGoog+numExt, numGoog, numExt)
}
}
}
// quarter returns a quarter of a year, in the form "2017q1".
func quarter(t time.Time) string {
// TODO: do this allocation-free? preculate them in init?
return fmt.Sprintf("%04dq%v", t.Year(), (int(t.Month()-1)/3)+1)
}
func (sc *statsClient) githubIssueCloseStats() {
repo := sc.corpus().GitHub().Repo("golang", "go")
if repo == nil {
log.Fatal("Failed to find Go repo.")
}
commClosed := map[string]map[*gophers.Person]int{}
googClosed := map[string]map[*gophers.Person]int{}
quarterSet := map[string]struct{}{}
repo.ForeachIssue(func(gi *maintner.GitHubIssue) error {
if !gi.Closed {
return nil
}
gi.ForeachEvent(func(e *maintner.GitHubIssueEvent) error {
if e.Type != "closed" {
return nil
}
if e.Actor == nil {
return nil
}
q := quarter(e.Created)
quarterSet[q] = struct{}{}
if commClosed[q] == nil {
commClosed[q] = map[*gophers.Person]int{}
}
if googClosed[q] == nil {
googClosed[q] = map[*gophers.Person]int{}
}
var p *gophers.Person
if e.Actor.Login == "gopherbot" {
gc := sc.corpus().GitCommit(e.CommitID)
if gc != nil {
email := gc.Author.Email()
p = gophers.GetPerson(email)
if p == nil {
log.Printf("unknown closer email: %q", email)
}
}
} else {
p = gophers.GetPerson("@" + e.Actor.Login)
}
if p != nil {
if p.Googler {
googClosed[q][p]++
} else {
commClosed[q][p]++
}
}
return nil
})
return nil
})
sumPeeps := func(m map[*gophers.Person]int) (sum int) {
for _, v := range m {
sum += v
}
return
}
var quarters []string
for q := range quarterSet {
quarters = append(quarters, q)
}
sort.Strings(quarters)
for _, q := range quarters {
googTotal := sumPeeps(googClosed[q])
commTotal := sumPeeps(commClosed[q])
googUniq := len(googClosed[q])
commUniq := len(commClosed[q])
tot := googTotal + commTotal
totUniq := googUniq + commUniq
percentGoog := 100 * float64(googTotal) / float64(tot)
fmt.Printf("%s closed issues: %v closes (%.2f%% goog %d; ext %d), %d unique people (%d goog, %d ext)\n",
q, tot,
percentGoog, googTotal, commTotal,
totUniq, googUniq, commUniq,
)
}
}
type personSet struct {
s map[*gophers.Person]struct{}
numGoog int
numExt int
}
func (s *personSet) sum() int { return len(s.s) }
func (s *personSet) add(p *gophers.Person) {
if s.s == nil {
s.s = make(map[*gophers.Person]struct{})
}
if _, ok := s.s[p]; !ok {
s.s[p] = struct{}{}
if p.Googler {
s.numGoog++
} else {
s.numExt++
}
}
}
func (sc *statsClient) githubGroups() {
ctx := context.Background()
ghc := sc.github()
teamList, _, err := ghc.Repositories.ListTeams(ctx, "golang", "go", nil)
if err != nil {
log.Fatal(err)
}
var teams = map[string]*personSet{}
for _, t := range teamList {
teamName := t.GetName()
switch teamName {
default:
continue
case "go-approvers", "gophers":
}
ps := new(personSet)
teams[teamName] = ps
users, _, err := ghc.Teams.ListTeamMembers(ctx, t.GetID(), &github.TeamListTeamMembersOptions{
ListOptions: github.ListOptions{PerPage: 1000},
})
if err != nil {
log.Fatal(err)
}
for _, u := range users {
login := strings.ToLower(u.GetLogin())
if login == "gopherbot" {
continue
}
p := gophers.GetPerson("@" + login)
if p == nil {
panic(fmt.Sprintf("failed to find github person %q", "@"+login))
}
ps.add(p)
}
}
cur := teams["go-approvers"]
prev := parseOldSnapshot(githubGoApprovers20170106)
log.Printf("Approvers 2016-12-13: %d: %v goog, %v ext", prev.sum(), prev.numGoog, prev.numExt)
log.Printf("Approvers cur: %d: %v goog, %v ext", cur.sum(), cur.numGoog, cur.numExt)
}
func parseOldSnapshot(s string) *personSet {
ps := new(personSet)
for _, f := range strings.Fields(s) {
if !strings.HasPrefix(f, "@") {
continue
}
p := gophers.GetPerson(f)
if p == nil {
panic(fmt.Sprintf("failed to find github person %q", f))
}
ps.add(p)
}
return ps
}
// Gerrit 2016-12-13:
// May start trybots, non-Googlers: 11
// Approvers, non-Googlers: 19
const githubGoApprovers20170106 = `
@0intro
0intro
David du Colombier
@4ad
4ad
Aram Hăvărneanu
@adams-sarah
adams-sarah
Sarah Adams
@adg
adg Owner
Andrew Gerrand
@alexbrainman
alexbrainman
Alex Brainman
@ality
ality
Anthony Martin
@campoy
campoy
Francesc Campoy
@DanielMorsing
DanielMorsing
Daniel Morsing
@davecheney
davecheney
Dave Cheney
@davidlazar
davidlazar
David Lazar
@dvyukov
dvyukov
Dmitry Vyukov
@eliasnaur
eliasnaur
Elias Naur
@hanwen
hanwen
Han-Wen Nienhuys
@josharian
josharian
Josh Bleecher Snyder
@jpoirier
jpoirier
Joseph Poirier
@kardianos
kardianos
Daniel Theophanes
@martisch
martisch
Martin Möhrmann
@matloob
matloob
Michael Matloob
@mdempsky
mdempsky
Matthew Dempsky
@mikioh
mikioh
Mikio Hara
@minux
minux
Minux Ma
@mwhudson
mwhudson
Michael Hudson-Doyle
@neild
neild
Damien Neil
@niemeyer
niemeyer
Gustavo Niemeyer
@odeke-em
odeke-em
Emmanuel T Odeke
@quentinmit
quentinmit Owner
Quentin Smith
@rakyll
rakyll
jbd@
@remyoudompheng
remyoudompheng
Rémy Oudompheng
@rminnich
rminnich
ron minnich
@rogpeppe
rogpeppe
Roger Peppe
@rui314
rui314
Rui Ueyama
@thanm
thanm
Than McIntosh
`
const githubGoAssignees20170106 = `
@crawshaw
crawshaw Team maintainer
David Crawshaw
@0intro
0intro
David du Colombier
@4ad
4ad
Aram Hăvărneanu
@adams-sarah
adams-sarah
Sarah Adams
@alexbrainman
alexbrainman
Alex Brainman
@alexcesaro
alexcesaro
Alexandre Cesaro
@ality
ality
Anthony Martin
@artyom
artyom
Artyom Pervukhin
@bcmills
bcmills
Bryan C. Mills
@billotosyr
billotosyr
@brtzsnr
brtzsnr
Alexandru Moșoi
@bsiegert
bsiegert
Benny Siegert
@c4milo
c4milo
Camilo Aguilar
@carl-mastrangelo
carl-mastrangelo
Carl Mastrangelo
@cespare
cespare
Caleb Spare
@DanielMorsing
DanielMorsing
Daniel Morsing
@davecheney
davecheney
Dave Cheney
@dominikh
dominikh
Dominik Honnef
@dskinner
dskinner
Daniel Skinner
@dsnet
dsnet
Joe Tsai
@dspezia
dspezia
Didier Spezia
@eliasnaur
eliasnaur
Elias Naur
@emergencybutter
emergencybutter
Arnaud
@evandbrown
evandbrown
Evan Brown
@fatih
fatih
Fatih Arslan
@garyburd
garyburd
Gary Burd
@hanwen
hanwen
Han-Wen Nienhuys
@jeffallen
jeffallen
Jeff R. Allen
@johanbrandhorst
johanbrandhorst
Johan Brandhorst
@josharian
josharian
Josh Bleecher Snyder
@jtsylve
jtsylve
Joe Sylve
@kardianos
kardianos
Daniel Theophanes
@kytrinyx
kytrinyx
Katrina Owen
@marete
marete
Brian Gitonga Marete
@martisch
martisch
Martin Möhrmann
@mattn
mattn
mattn
@mdempsky
mdempsky
Matthew Dempsky
@mdlayher
mdlayher
Matt Layher
@mikioh
mikioh
Mikio Hara
@millerresearch
millerresearch
Richard Miller
@minux
minux
Minux Ma
@mundaym
mundaym
Michael Munday
@mwhudson
mwhudson
Michael Hudson-Doyle
@myitcv
myitcv
Paul Jolly
@neelance
neelance
Richard Musiol
@niemeyer
niemeyer
Gustavo Niemeyer
@nodirt
nodirt
Nodir Turakulov
@rahulchaudhry
rahulchaudhry
Rahul Chaudhry
@rauls5382
rauls5382
Raul Silvera
@remyoudompheng
remyoudompheng
Rémy Oudompheng
@rhysh
rhysh
Rhys Hiltner
@rogpeppe
rogpeppe
Roger Peppe
@rsc
rsc Owner
Russ Cox
@rui314
rui314
Rui Ueyama
@sbinet
sbinet
Sebastien Binet
@shawnps
shawnps
Shawn Smith
@thanm
thanm
Than McIntosh
@titanous
titanous
Jonathan Rudenberg
@tombergan
tombergan
@tzneal
tzneal
Todd
@vstefanovic
vstefanovic
@wathiede
wathiede
Bill
@x1ddos
x1ddos
alex
@zombiezen
zombiezen
Ross Light
`
var discoverGoRepo = flag.String("discovery-go-repo", "go", "github.com/golang repo to discovery email addreses from")
func foreachProjectUnsorted(g *maintner.Gerrit, f func(gp *maintner.GerritProject) error) {
g.ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
if !gerritProjects.Includes(gp.Project()) {
log.Printf("skipping project %s", gp.Project())
return nil
}
return f(gp)
})
}
func (sc *statsClient) findGerritGophers() {
gerrc := sc.gerrit()
log.Printf("find gerrit gophers")
gerritEmails := map[string]int{}
const suffix = "@62eb7196-b449-3ce5-99f1-c037f21e1705"
foreachProjectUnsorted(sc.corpus().Gerrit(), func(gp *maintner.GerritProject) error {
return gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
for _, meta := range cl.Metas {
who := meta.Commit.Author.Email()
if strings.HasSuffix(who, suffix) {
gerritEmails[who]++
}
}
return nil
})
})
var emails []string
for k := range gerritEmails {
emails = append(emails, k)
}
sort.Slice(emails, func(i, j int) bool {
return gerritEmails[emails[j]] < gerritEmails[emails[i]]
})
for _, email := range emails {
p := gophers.GetPerson(email)
if p == nil {
ai, err := gerrc.GetAccountInfo(context.Background(), strings.TrimSuffix(email, suffix))
if err != nil {
log.Printf("Looking up %s: %v", email, err)
continue
}
fmt.Printf("addPerson(%q, %q, %q)\n", ai.Name, ai.Email, email)
}
}
}
func (sc *statsClient) findGithubEmails() {
ghc := sc.github()
seen := map[string]bool{}
for page := 1; page < 500; page++ {
commits, _, err := ghc.Repositories.ListCommits(context.Background(), "golang", *discoverGoRepo, &github.CommitsListOptions{
ListOptions: github.ListOptions{Page: page, PerPage: 1000},
})
if err != nil {
log.Fatalf("page %d: %v", page, err)
}
for _, com := range commits {
ghUser := com.Author.GetLogin()
if ghUser == "" {
continue
}
if seen[ghUser] {
continue
}
seen[ghUser] = true
ca := com.Commit.Author
p := gophers.GetPerson("@" + ghUser)
if p != nil && gophers.GetPerson(ca.GetEmail()) == p {
// Nothing new.
continue
}
fmt.Printf("addPerson(%q, %q, %q)\n", ca.GetName(), ca.GetEmail(), "@"+ghUser)
}
}
}
func (sc *statsClient) gerritCLStats() {
perQuarter := map[string]int{}
perQuarterGoog := map[string]int{}
perQuarterExt := map[string]int{}
printedUnknown := map[string]bool{}
perQuarterUniq := map[string]*personSet{}
foreachProjectUnsorted(sc.corpus().Gerrit(), func(gp *maintner.GerritProject) error {
gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
q := quarter(cl.Created)
perQuarter[q]++
email := cl.Commit.Author.Email()
p := gophers.GetPerson(email)
var isGoog bool
if p != nil {
isGoog = p.Googler
if _, ok := perQuarterUniq[q]; !ok {
perQuarterUniq[q] = new(personSet)
}
perQuarterUniq[q].add(p)
} else {
isGoog = strings.HasSuffix(email, "@google.com")
if !printedUnknown[email] {
printedUnknown[email] = true
fmt.Printf("addPerson(%q, %q)\n", cl.Commit.Author.Name(), email)
}
}
if isGoog {
perQuarterGoog[q]++
} else {
perQuarterExt[q]++
}
return nil
})
return nil
})
for _, q := range sortedStrMapKeys(perQuarter) {
goog := perQuarterGoog[q]
ext := perQuarterExt[q]
tot := goog + ext
fmt.Printf("%s: %d commits (%0.2f%% %d goog, %d ext)\n", q, perQuarter[q], 100*float64(goog)/float64(tot), goog, ext)
}
for _, q := range sortedStrMapKeys(perQuarter) {
ps := perQuarterUniq[q]
fmt.Printf("%s: %d unique users (%0.2f%% %d goog, %d ext)\n", q, len(ps.s), 100*float64(ps.numGoog)/float64(len(ps.s)), ps.numGoog, ps.numExt)
}
}
func sortedStrMapKeys(m map[string]int) []string {
ret := make([]string, 0, len(m))
for k := range m {
ret = append(ret, k)
}
sort.Strings(ret)
return ret
}
func (sc *statsClient) workshopStats() {
const workshopIssue = 21017
loc, err := time.LoadLocation("America/Denver")
if err != nil {
fmt.Fprintf(os.Stderr, "loading location failed: %v", err)
os.Exit(2)
}
workshopStartDate := time.Date(2017, time.July, 15, 0, 0, 0, 0, loc)
// The key is the string representation of the gerrit ID.
// The value is the string for the GitHub login.
contributors := map[string]string{}
// Get all the contributors from comments on the issue.
sc.corpus().GitHub().Repo("golang", "go").Issue(workshopIssue).ForeachComment(func(c *maintner.GitHubComment) error {
contributors[strings.TrimSpace(c.Body)] = c.User.Login
return nil
})
fmt.Printf("Number of registrations: %d\n", len(contributors))
// Store the already known contributors before the workshop.
knownContributors := map[string]struct{}{}
type projectStats struct {
name string
openedCLs []string // Gerrit IDs of owners of opened CLs
mergedCLs []string // Gerrit IDs of owners of merged CLs
}
ps := []projectStats{}
// Get all the CLs during the time of the workshop and after.
foreachProjectUnsorted(sc.corpus().Gerrit(), func(gp *maintner.GerritProject) error {
p := projectStats{
name: gp.Project(),
}
gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
ownerID := fmt.Sprintf("%d", cl.OwnerID())
// Make sure it was made after the workshop started
// otherwise save as a known contributor.
if cl.Created.After(workshopStartDate) {
if _, ok := contributors[ownerID]; ok {
p.openedCLs = append(p.openedCLs, ownerID)
if cl.Status == "merged" {
p.mergedCLs = append(p.mergedCLs, ownerID)
}
}
} else {
knownContributors[ownerID] = struct{}{}
}
return nil
})
// Return early if no one contributed to that project.
if len(p.openedCLs) == 0 && len(p.mergedCLs) == 0 {
return nil
}
ps = append(ps, p)
return nil
})
sort.Slice(ps, func(i, j int) bool { return ps[i].name < ps[j].name })
for _, p := range ps {
var newOpened, newMerged int
// Determine the first time contributors.
for _, id := range p.openedCLs {
if _, ok := knownContributors[id]; !ok {
newOpened++
}
}
for _, id := range p.mergedCLs {
if _, ok := knownContributors[id]; !ok {
newMerged++
}
}
// Ignore repos where only past contributors had patches merged.
if newOpened != 0 || newMerged != 0 {
fmt.Printf(`%s:
Total Opened CLs: %d
Total Merged CLs: %d
New Contributors Opened CLs: %d
New Contributors Merged CLs: %d`+"\n", p.name, len(p.openedCLs), len(p.mergedCLs), newOpened, newMerged)
}
}
}
func (sc *statsClient) rangeStats() {
var (
newCLs = map[*gophers.Person]int{}
commentsOnOtherCLs = map[*gophers.Person]int{}
githubIssuesCreated = map[*gophers.Person]int{}
githubUniqueIssueComments = map[*gophers.Person]int{} // non-owner
githubUniqueIssueEvents = map[*gophers.Person]int{} // non-owner
uniqueFilesEdited = map[*gophers.Person]int{}
uniqueDirsEdited = map[*gophers.Person]int{}
)
t1 := *startTime
t2 := *endTime
sc.corpus().GitHub().ForeachRepo(func(r *maintner.GitHubRepo) error {
if r.ID().Owner != "golang" {
return nil
}
return r.ForeachIssue(func(gi *maintner.GitHubIssue) error {
if gi.User == nil {
return nil
}
owner := gophers.GetPerson("@" + gi.User.Login)
if gi.Created.After(t1) && gi.Created.Before(t2) {
if owner == nil {
log.Printf("No owner for golang.org/issue/%d (%q)", gi.Number, gi.User.Login)
} else if !owner.Bot {
githubIssuesCreated[owner]++
}
}
sawCommenter := map[*gophers.Person]bool{}
gi.ForeachComment(func(gc *maintner.GitHubComment) error {
if gc.User == nil || gc.User.ID == gi.User.ID {
return nil
}
if gc.Created.After(t1) && gc.Created.Before(t2) {
commenter := gophers.GetPerson("@" + gc.User.Login)
if commenter == nil || sawCommenter[commenter] || commenter.Bot {
return nil
}
sawCommenter[commenter] = true
githubUniqueIssueComments[commenter]++
}
return nil
})
sawEventer := map[*gophers.Person]bool{}
gi.ForeachEvent(func(gc *maintner.GitHubIssueEvent) error {
if gc.Actor == nil || gc.Actor.ID == gi.User.ID {
return nil
}
if gc.Created.After(t1) && gc.Created.Before(t2) {
eventer := gophers.GetPerson("@" + gc.Actor.Login)
if eventer == nil || sawEventer[eventer] || eventer.Bot {
return nil
}
sawEventer[eventer] = true
githubUniqueIssueEvents[eventer]++
}
return nil
})
return nil
})
})
type projectFile struct {
gp *maintner.GerritProject
file string
}
var fileTouched = map[*gophers.Person]map[projectFile]bool{}
var dirTouched = map[*gophers.Person]map[projectFile]bool{}
foreachProjectUnsorted(sc.corpus().Gerrit(), func(gp *maintner.GerritProject) error {
if gp.Server() != "go.googlesource.com" {
return nil
}
return gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
owner := gophers.GetPerson(cl.Commit.Author.Email())
if cl.Created.After(t1) && cl.Created.Before(t2) {
newCLs[owner]++
}
if ct := cl.Commit.CommitTime; ct.After(t1) && ct.Before(t2) && cl.Status == "merged" {
email := cl.Commit.Author.Email() // gerrit-y email
who := gophers.GetPerson(email)
if who != nil {
if len(cl.Commit.Files) > 20 {
// Probably just a cleanup or moving files, skip this CL.
return nil
}
if fileTouched[who] == nil {
fileTouched[who] = map[projectFile]bool{}
}
if dirTouched[who] == nil {
dirTouched[who] = map[projectFile]bool{}
}
for _, diff := range cl.Commit.Files {
if strings.Contains(diff.File, "vendor/") {
continue
}
fileTouched[who][projectFile{gp, diff.File}] = true
dirTouched[who][projectFile{gp, path.Dir(diff.File)}] = true
}
}
}
saw := map[*gophers.Person]bool{}
for _, meta := range cl.Metas {
t := meta.Commit.CommitTime
if t.Before(t1) || t.After(t2) {
continue
}
email := meta.Commit.Author.Email() // gerrit-y email
who := gophers.GetPerson(email)
if who == owner || who == nil || saw[who] || who.Bot {
continue
}
saw[who] = true
commentsOnOtherCLs[who]++
}
return nil
})
})
for p, m := range fileTouched {
uniqueFilesEdited[p] = len(m)
}
for p, m := range dirTouched {
uniqueDirsEdited[p] = len(m)
}
top(newCLs, "CLs created:", 40)
top(commentsOnOtherCLs, "Unique non-self CLs commented on:", 40)
top(githubIssuesCreated, "GitHub issues created:", 40)
top(githubUniqueIssueComments, "Unique GitHub issues commented on:", 40)
top(githubUniqueIssueEvents, "Unique GitHub issues acted on:", 40)
top(uniqueFilesEdited, "Unique files edited:", 40)
top(uniqueDirsEdited, "Unique directories edited:", 40)
}
func top(m map[*gophers.Person]int, title string, n int) {
var kk []*gophers.Person
for k := range m {
if k == nil {
continue
}
kk = append(kk, k)
}
sort.Slice(kk, func(i, j int) bool { return m[kk[j]] < m[kk[i]] })
fmt.Println(title)
for i, k := range kk {
if i == n {
break
}
fmt.Printf(" %5d %s\n", m[k], k.Name)
}
fmt.Println()
}
func getGithubToken() (string, error) {
// TODO: get from GCE metadata, etc.
tokenFile := filepath.Join(os.Getenv("HOME"), "keys", "github-read-org")
slurp, err := ioutil.ReadFile(tokenFile)
if err != nil {
return "", err
}
f := strings.SplitN(strings.TrimSpace(string(slurp)), ":", 2)
if len(f) != 2 || f[0] == "" || f[1] == "" {
return "", fmt.Errorf("Expected token file %s to be of form <username>:<token>", tokenFile)
}
return f[1], nil
}
func getGithubClient() (*github.Client, error) {
token, err := getGithubToken()
if err != nil {
return nil, err
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(context.Background(), ts)
return github.NewClient(tc), nil
}