blob: aca1631c7ca91beae44d7303df028b84c298c251 [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package db is responsible for getting information about CLs and PRs in Gerrit
// and GitHub respectively.
package db
import (
"context"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/andygrunwald/go-gerrit"
"github.com/google/go-github/github"
)
// RegenAttempt represents either a genproto regen PR or a gocloud gapic regen
// CL.
type RegenAttempt interface {
Author() string
Title() string
URL() string
Created() time.Time
Open() bool
}
// ByCreated allows RegenAttempt to be sorted by Created field.
type ByCreated []RegenAttempt
func (a ByCreated) Len() int { return len(a) }
func (a ByCreated) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByCreated) Less(i, j int) bool { return a[i].Created().After(a[j].Created()) }
// regenAttempt represents either a genproto regen PR or a gocloud gapic regen
// CL.
type regenAttempt struct {
author string
title string
url string
created time.Time
open bool
}
func (ra *regenAttempt) Author() string { return ra.author }
func (ra *regenAttempt) Title() string { return ra.title }
func (ra *regenAttempt) URL() string { return ra.url }
func (ra *regenAttempt) Created() time.Time { return ra.created }
func (ra *regenAttempt) Open() bool { return ra.open }
// GerritRegenAttempt is a gerrit regen attempt (a CL).
type GerritRegenAttempt struct {
regenAttempt
ChangeID string
}
// GenprotoRegenAttempt is a genproto regen attempt (a PR).
type GenprotoRegenAttempt struct {
regenAttempt
}
// Db can communicate with GitHub and Gerrit to get PRs / CLs.
type Db struct {
gerritClient *gerrit.Client
githubClient *github.Client
cacheMu sync.Mutex
// For some reason, the Changes API only returns AccountID. So we cache the
// accountID->name to improve performance / reduce adtl calls.
cachedGerritAccounts map[int]string // accountid -> name
}
// New returns a new Db.
func New(ctx context.Context, githubClient *github.Client, gerritClient *gerrit.Client) *Db {
db := &Db{
githubClient: githubClient,
gerritClient: gerritClient,
cacheMu: sync.Mutex{},
cachedGerritAccounts: map[int]string{},
}
return db
}
// GetPRs fetches regen PRs from genproto.
func (db *Db) GetPRs(ctx context.Context) ([]RegenAttempt, error) {
log.Println("getting genproto changes")
genprotoPRs := []RegenAttempt{}
// We don't bother paginating, because it hurts our requests quota and makes
// the page slower without a lot of value.
opt := &github.PullRequestListOptions{
ListOptions: github.ListOptions{PerPage: 50},
State: "all",
}
prs, _, err := db.githubClient.PullRequests.List(ctx, "googleapis", "go-genproto", opt)
if err != nil {
return nil, err
}
for _, pr := range prs {
if !strings.Contains(pr.GetTitle(), "regen") {
continue
}
genprotoPRs = append(genprotoPRs, &GenprotoRegenAttempt{
regenAttempt: regenAttempt{
author: pr.GetUser().GetLogin(),
title: pr.GetTitle(),
url: pr.GetHTMLURL(),
created: pr.GetCreatedAt(),
open: pr.GetState() == "open",
},
})
}
return genprotoPRs, nil
}
// GetCLs fetches regen CLs from Gerrit.
func (db *Db) GetCLs(ctx context.Context) ([]RegenAttempt, error) {
log.Println("getting gocloud changes")
gocloudCLs := []RegenAttempt{}
changes, _, err := db.gerritClient.Changes.QueryChanges(&gerrit.QueryChangeOptions{
QueryOptions: gerrit.QueryOptions{Query: []string{"project:gocloud"}, Limit: 200},
})
if err != nil {
return nil, err
}
for _, c := range *changes {
if !strings.Contains(c.Subject, "regen") {
continue
}
// For some reason, the Changes API only returns AccountID. So now we
// have to go get the name.
db.cacheMu.Lock()
if _, ok := db.cachedGerritAccounts[c.Owner.AccountID]; !ok {
log.Println("looking up user", c.Owner.AccountID)
ai, resp, err := db.gerritClient.Accounts.GetAccount(strconv.Itoa(c.Owner.AccountID))
if err != nil {
if resp.StatusCode == http.StatusNotFound {
db.cachedGerritAccounts[c.Owner.AccountID] = fmt.Sprintf("unknown user account ID: %d\n", c.Owner.AccountID)
} else {
db.cacheMu.Unlock()
return nil, err
}
} else {
db.cachedGerritAccounts[c.Owner.AccountID] = ai.Email
}
}
gocloudCLs = append(gocloudCLs, &GerritRegenAttempt{
regenAttempt: regenAttempt{
author: db.cachedGerritAccounts[c.Owner.AccountID],
title: c.Subject,
url: fmt.Sprintf("https://code-review.googlesource.com/q/%s", c.ChangeID),
created: c.Created.Time,
open: c.Status == "NEW",
},
ChangeID: c.ChangeID,
})
db.cacheMu.Unlock()
}
return gocloudCLs, nil
}
// FirstOpen returns the first open regen attempt.
func FirstOpen(ras []RegenAttempt) (RegenAttempt, bool) {
for _, ra := range ras {
if ra.Open() {
return ra, true
}
}
return nil, false
}