| // 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 |
| } |