2016-08-30 01:58:31 +03:00
|
|
|
// Copyright 2016 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 godash
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"sort"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/google/go-github/github"
|
|
|
|
"golang.org/x/net/context"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Update fetches new information from GitHub for any issues modified since s was last updated.
|
|
|
|
func (s *Stats) Update(ctx context.Context, gh *github.Client, log func(string, ...interface{})) error {
|
2017-02-21 01:12:04 +03:00
|
|
|
res, err := listIssues(ctx, gh, github.IssueListByRepoOptions{State: "all", Since: s.Since})
|
2016-08-30 01:58:31 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
log("Github returned %d issues", len(res))
|
|
|
|
if s.Issues == nil {
|
|
|
|
log("Initializing new data")
|
|
|
|
s.Issues = make(map[int]*IssueStat)
|
|
|
|
}
|
|
|
|
for _, issue := range res {
|
|
|
|
s.Issues[issue.Number] = &IssueStat{
|
|
|
|
Created: issue.Created,
|
|
|
|
Closed: issue.Closed,
|
|
|
|
Updated: issue.Updated,
|
|
|
|
Milestone: issue.Milestone,
|
|
|
|
}
|
|
|
|
|
|
|
|
if issue.Updated.After(s.Since) {
|
|
|
|
s.Since = issue.Updated
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Ingest event details. We have to do this last because it
|
|
|
|
// can blow through our rate limit.
|
|
|
|
var issuenums []int
|
|
|
|
for n, issue := range s.Issues {
|
|
|
|
if !issue.Updated.Before(s.IssueDetailSince) {
|
|
|
|
issuenums = append(issuenums, n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(issuenums) == 0 {
|
|
|
|
log("No new issues; not updating")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
sort.Sort(issueUpdatedSort{issuenums, s.Issues})
|
|
|
|
|
|
|
|
log("Need to update %d issues", len(issuenums))
|
|
|
|
|
|
|
|
// TODO: Limit by time instead of a fixed cap?
|
|
|
|
if len(issuenums) > 1000 {
|
|
|
|
issuenums = issuenums[:1000]
|
|
|
|
}
|
|
|
|
|
|
|
|
numch := make(chan int)
|
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
|
|
|
|
|
|
for i := 0; i < 5; i++ {
|
|
|
|
g.Go(func() error {
|
|
|
|
for num := range numch {
|
2017-02-21 01:12:04 +03:00
|
|
|
if err := s.UpdateIssue(ctx, gh, num, log); err != nil {
|
2016-08-30 01:58:31 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
g.Go(func() error {
|
|
|
|
defer close(numch)
|
|
|
|
for _, num := range issuenums {
|
|
|
|
select {
|
|
|
|
case numch <- num:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err := g.Wait(); err != nil {
|
|
|
|
// Failure to update issue details should not cause the whole update to fail.
|
|
|
|
log("Failed updating stats: %v", err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
s.IssueDetailSince = s.Issues[issuenums[len(issuenums)-1]].Updated
|
|
|
|
log("Updated %d issues. Details now correct through %v", len(issuenums), s.IssueDetailSince)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateIssue updates a single issue, without moving s.Since.
|
2017-02-21 01:12:04 +03:00
|
|
|
func (s *Stats) UpdateIssue(ctx context.Context, gh *github.Client, num int, log func(string, ...interface{})) error {
|
2016-08-30 01:58:31 +03:00
|
|
|
issue := s.Issues[num]
|
|
|
|
var milestone string
|
|
|
|
var labels []string
|
|
|
|
milestoneChange := func(m string, t time.Time) {
|
|
|
|
if milestone != "" && m != milestone {
|
|
|
|
issue.MilestoneHistory = append(issue.MilestoneHistory, MilestoneChange{milestone, t})
|
|
|
|
}
|
|
|
|
milestone = m
|
|
|
|
}
|
|
|
|
for page := 1; ; {
|
2017-02-21 01:12:04 +03:00
|
|
|
events, resp, err := gh.Issues.ListIssueEvents(ctx, projectOwner, projectRepo, num, &github.ListOptions{
|
2016-08-30 01:58:31 +03:00
|
|
|
Page: page,
|
|
|
|
PerPage: 100,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
// TODO: Sometimes calls to GitHub seem to time out; if they do, perhaps we should retry?
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if page == 1 {
|
|
|
|
issue.MilestoneHistory = nil
|
|
|
|
}
|
|
|
|
for _, event := range events {
|
|
|
|
evtype := getString(event.Event)
|
|
|
|
evtime := getTime(event.CreatedAt)
|
|
|
|
switch evtype {
|
|
|
|
case "labeled":
|
|
|
|
if event.Label == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
label := getString(event.Label.Name)
|
|
|
|
labels = append(labels, label)
|
|
|
|
switch label {
|
|
|
|
case "fixed", "retracted", "done", "duplicate", "workingasintended", "wontfix", "invalid", "unfortunate", "timedout":
|
|
|
|
// Old issues have these labels.
|
|
|
|
issue.Closed = evtime
|
|
|
|
}
|
|
|
|
if label[:2] == "go" {
|
|
|
|
milestoneChange("Go"+label[2:], evtime)
|
|
|
|
}
|
|
|
|
case "milestoned":
|
|
|
|
if event.Milestone == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
m := getString(event.Milestone.Title)
|
|
|
|
milestoneChange(m, evtime)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (resp.Remaining * 2) < (resp.Limit / 3) {
|
|
|
|
// Save rate limit for the more important updates above.
|
|
|
|
log("Out of quota (%d/%d) until %v", resp.Remaining, resp.Limit, resp.Reset)
|
|
|
|
return errors.New("out of quota")
|
|
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
page = resp.NextPage
|
|
|
|
}
|
|
|
|
// GitHub has a massive number of issues closed on 2014/12/8;
|
|
|
|
// I suspect this is when they first added the closed
|
|
|
|
// field. If we still think the issue is closed on this date,
|
|
|
|
// that probably means we failed to correctly process the
|
|
|
|
// labels. Log the issue's labels so we can investigate and
|
|
|
|
// possibly add to the list of labels above.
|
|
|
|
c := issue.Closed
|
|
|
|
if c.Year() == 2014 && c.Month() == 12 && c.Day() == 8 {
|
|
|
|
log("Issue %d labels: %v", num, labels)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type issueUpdatedSort struct {
|
|
|
|
nums []int
|
|
|
|
issues map[int]*IssueStat
|
|
|
|
}
|
|
|
|
|
|
|
|
func (x issueUpdatedSort) Len() int { return len(x.nums) }
|
|
|
|
func (x issueUpdatedSort) Swap(i, j int) { x.nums[i], x.nums[j] = x.nums[j], x.nums[i] }
|
|
|
|
func (x issueUpdatedSort) Less(i, j int) bool {
|
|
|
|
return x.issues[x.nums[i]].Updated.Before(x.issues[x.nums[j]].Updated)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stats contains information about all GitHub issues.
|
|
|
|
//
|
|
|
|
// We track statistics for each issue to produce graphs:
|
|
|
|
// - Issue creation time
|
|
|
|
// - TODO(quentin): First reply time from Go team member
|
|
|
|
// - Issue close time
|
|
|
|
// - Issue current milestone
|
|
|
|
// - History of issue labels + milestones
|
|
|
|
// As well as the following global info
|
|
|
|
// - Last issue update time
|
|
|
|
// - Last issue detail update time
|
|
|
|
type Stats struct {
|
|
|
|
// Issues is a map of issue number to per-issue data.
|
|
|
|
Issues map[int]*IssueStat
|
|
|
|
// Since is the high watermark for issue update times; any
|
|
|
|
// issues updated since Since will be refetched.
|
|
|
|
Since time.Time
|
|
|
|
// IssueDetailSince is the high watermark for issue details;
|
|
|
|
// this is separate because requesting issue details uses up
|
|
|
|
// quota, and we cannot request all issues at once.
|
|
|
|
IssueDetailSince time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// MilestoneChange stores a historical milestone. We store historical
|
|
|
|
// milestones separately since most issues have only ever had one
|
|
|
|
// milestone; we can save on constructing and serializing the slice
|
|
|
|
// then.
|
|
|
|
type MilestoneChange struct {
|
|
|
|
// Name is the name of the milestone.
|
|
|
|
Name string
|
|
|
|
// Until is the time that the milestone was removed.
|
|
|
|
Until time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// IssueStat holds an individual issue's important facts.
|
|
|
|
type IssueStat struct {
|
|
|
|
Created, Closed, Updated time.Time
|
|
|
|
// Milestone contains the milestone the issue is currently
|
|
|
|
// associated with.
|
|
|
|
Milestone string
|
|
|
|
// MilestoneHistory contains previous milestones and the time
|
|
|
|
// the issue ceased to be assigned to that milestone. We store
|
|
|
|
// this so the slice can be empty for most issues that have
|
|
|
|
// only ever been associated with one milestone.
|
|
|
|
MilestoneHistory []MilestoneChange
|
|
|
|
}
|