cmd/gopherstats: delete

We haven't used this code in many years. Remove it as unused.
If a specific need comes up, we can get it back from history.
Doing that isn't much harder compared to figuring out how to
use it after all those years.

For golang/go#51867.
Fixes golang/go#34259.
Fixes golang/go#27632.

Change-Id: Icda2c077fd10c48812e2e59b8d19386abd7f9357
Reviewed-on: https://go-review.googlesource.com/c/build/+/394517
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/cmd/gopherstats/README.md b/cmd/gopherstats/README.md
deleted file mode 100644
index 1987c48..0000000
--- a/cmd/gopherstats/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-<!-- Auto-generated by x/build/update-readmes.go -->
-
-[![GoDoc](https://godoc.org/golang.org/x/build/cmd/gopherstats?status.svg)](https://godoc.org/golang.org/x/build/cmd/gopherstats)
-
-# golang.org/x/build/cmd/gopherstats
-
-
diff --git a/cmd/gopherstats/gopherstats.go b/cmd/gopherstats/gopherstats.go
deleted file mode 100644
index a791056..0000000
--- a/cmd/gopherstats/gopherstats.go
+++ /dev/null
@@ -1,1202 +0,0 @@
-// 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
-}