// summarizeIssue fetches a single codereview issue and all its // comments and summarizes its state by reading the comments. func summarizeIssue(env task.Env, id int) (im issueMeta) { im.issue = id im.policyVersion = policyVersion url := urlWithParams(messagesQuery, map[string]string{ "ISSUE_NUMBER": strconv.Itoa(id), }) c := env.HTTPClient() res, err := c.Get(url) env.Logf("Fetching %s = %v", url, err) if err != nil { im.err = err return } defer res.Body.Close() var r issueResult err = json.NewDecoder(res.Body).Decode(&r) if err != nil { im.err = err return } im.lastModified = r.Modified if r.Closed { im.reviewer = "close" return } open := true var lastMsg *message sort.Sort(byMessageDate(r.Messages)) for _, m := range r.Messages { lastMsg = m if m := reviewRx.FindStringSubmatch(m.Text); m != nil { if m[1] == "close" || m[1] == "closed" { open = false continue } open = true im.reviewer = m[1] continue } if strings.HasPrefix(m.Text, "PTAL") { open = true } } if lastMsg != nil { if m := qRx.FindStringSubmatch(lastMsg.Text); m != nil { cmd := m[1] if cmd == "wait" { open = false } } if strings.Contains(lastMsg.Text, "*** Submitted as http://code.google.com/p/go/source/") { open = false } } if !open { im.reviewer = "close" } return }
func (codereviewTask) Poll(env task.Env) (pts []*task.PolledTask, err error) { type res struct { pt []*task.PolledTask cc bool err error } var chs []chan res for _, cc := range []bool{false, true} { for _, month := range relevantMonths() { ch := make(chan res, 1) chs = append(chs, ch) go func(month string, cc bool) { pt, err := pollMonth(env, month, cc) env.Logf("Month %q (cc=%v) = %d, %v", month, cc, len(pt), err) ch <- res{pt, cc, err} }(month, cc) } } seen := make(map[string]bool) // ID -> true for _, ch := range chs { res := <-ch if res.err != nil { return nil, res.err } for _, pt := range res.pt { if !seen[pt.ID] { seen[pt.ID] = true pts = append(pts, pt) } } } return pts, nil }
func (issueTask) Poll(env task.Env) ([]*task.PolledTask, error) { c := env.HTTPClient() res, err := c.Get(query) if err != nil { return nil, err } defer res.Body.Close() issues, err := ParseIssues(res.Body) if err != nil { return nil, err } env.Logf("got %d issues", len(issues)) tasks := make([]*task.PolledTask, 0, len(issues)) for _, issue := range issues { if issue.Owner != nil { // Owned issues aren't logically open continue } t := &task.PolledTask{ Title: issue.Title, } if m := idFromURL.FindStringSubmatch(issue.ID); m != nil { t.ID = m[1] } else { env.Logf("Bogus ID %q", issue.ID) continue } tasks = append(tasks, t) } env.Logf("returning %d tasks", len(tasks)) return tasks, nil }
func pollMonth(env task.Env, month string, cc bool) (pt []*task.PolledTask, err error) { c := env.HTTPClient() cursor := "" n := 0 var ( metawg sync.WaitGroup // for meta or metaerr to be loaded meta monthMeta metaerr error ) metawg.Add(1) go func() { defer metawg.Done() meta, metaerr = getMonthMeta(env, month) }() // Opens issue to rietveld, even if they're logically closed // (R=closed) to us. var rietOpen []*Review for { query := monthQuery if cc { query = monthQueryCC } url := urlWithParams(query, map[string]string{ "DATE_AFTER": month + "-01 00:00:00", "DATE_BEFORE": monthAfter(month) + "-01 00:00:00", "CURSOR": cursor, "LIMIT": fmt.Sprint(itemsPerPage), }) n++ env.Logf("Fetching codereview page %d: %s", n, url) res, err := c.Get(url) if err != nil { env.Logf("Error fetching %s: %v", url, err) return nil, err } var reviews []*Review reviews, cursor, err = ParseReviews(res.Body) res.Body.Close() if err != nil { env.Logf("ParseReviews error: %v", err) return nil, err } env.Logf("got %d reviews, cursor is %q", len(reviews), cursor) for _, r := range reviews { if r.Issue == 0 { env.Logf("bogus issue %+v", r) continue } rietOpen = append(rietOpen, r) } if cursor == "" || len(reviews) < itemsPerPage { break } } metawg.Wait() if metaerr != nil { env.Logf("Error loading month meta for %q: %v", month, metaerr) return nil, metaerr } // For each issue that Rietveld believes to be open, see if we // have the latest comments for it and then see if it's // actually logically still open, based on its comments. var updates []chan issueMeta for _, r := range rietOpen { im := meta[r.Issue] if im.policyVersion != policyVersion || im.lastModified != r.Modified { env.Logf("Need to summarize issue %d", r.Issue) ch := make(chan issueMeta, 1) updates = append(updates, ch) go func(issueId int) { ch <- summarizeIssue(env, issueId) }(r.Issue) } else { env.Logf("Issue %d is unmodified.", r.Issue) } } var updateErr error var errCount int for _, ch := range updates { im := <-ch if im.err != nil { errCount++ if updateErr == nil { updateErr = im.err } } else { meta[im.issue] = im } } if updateErr != nil { // Save what we've got so far. env.Logf("Errors re-fetching month %q: %d/%d fetches failed. Saving %d good issue summaries.", month, errCount, len(updates), len(meta)) env.SetMeta(monthMetaPrefix+month, meta.serialize()) return nil, updateErr } if len(updates) > 0 { if err := env.SetMeta(monthMetaPrefix+month, meta.serialize()); err != nil { env.Logf("Error writing month meta for %q: %v", month, err) return nil, err } } for _, r := range rietOpen { im := meta[r.Issue] if im.reviewer == "close" { continue } ownerHint := im.reviewer if ownerHint == "" { ownerHint = r.Reviewer() } t := &task.PolledTask{ ID: fmt.Sprint(r.Issue), Title: r.Desc, OwnerHint: ownerHint, } pt = append(pt, t) } env.Logf("codereview: for month %q, returning %d open tasks", month, len(pt)) return pt, nil }