1// Copyright 2019 Google LLC 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package db is responsible for getting information about CLs and PRs in Gerrit 16// and GitHub respectively. 17package db 18 19import ( 20 "context" 21 "fmt" 22 "log" 23 "net/http" 24 "strconv" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/andygrunwald/go-gerrit" 30 "github.com/google/go-github/github" 31) 32 33// RegenAttempt represents either a genproto regen PR or a gocloud gapic regen 34// CL. 35type RegenAttempt interface { 36 Author() string 37 Title() string 38 URL() string 39 Created() time.Time 40 Open() bool 41} 42 43// ByCreated allows RegenAttempt to be sorted by Created field. 44type ByCreated []RegenAttempt 45 46func (a ByCreated) Len() int { return len(a) } 47func (a ByCreated) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 48func (a ByCreated) Less(i, j int) bool { return a[i].Created().After(a[j].Created()) } 49 50// regenAttempt represents either a genproto regen PR or a gocloud gapic regen 51// CL. 52type regenAttempt struct { 53 author string 54 title string 55 url string 56 created time.Time 57 open bool 58} 59 60func (ra *regenAttempt) Author() string { return ra.author } 61func (ra *regenAttempt) Title() string { return ra.title } 62func (ra *regenAttempt) URL() string { return ra.url } 63func (ra *regenAttempt) Created() time.Time { return ra.created } 64func (ra *regenAttempt) Open() bool { return ra.open } 65 66// GerritRegenAttempt is a gerrit regen attempt (a CL). 67type GerritRegenAttempt struct { 68 regenAttempt 69 ChangeID string 70} 71 72// GenprotoRegenAttempt is a genproto regen attempt (a PR). 73type GenprotoRegenAttempt struct { 74 regenAttempt 75} 76 77// Db can communicate with GitHub and Gerrit to get PRs / CLs. 78type Db struct { 79 gerritClient *gerrit.Client 80 githubClient *github.Client 81 82 cacheMu sync.Mutex 83 // For some reason, the Changes API only returns AccountID. So we cache the 84 // accountID->name to improve performance / reduce adtl calls. 85 cachedGerritAccounts map[int]string // accountid -> name 86} 87 88// New returns a new Db. 89func New(ctx context.Context, githubClient *github.Client, gerritClient *gerrit.Client) *Db { 90 db := &Db{ 91 githubClient: githubClient, 92 gerritClient: gerritClient, 93 94 cacheMu: sync.Mutex{}, 95 cachedGerritAccounts: map[int]string{}, 96 } 97 98 return db 99} 100 101// GetPRs fetches regen PRs from genproto. 102func (db *Db) GetPRs(ctx context.Context) ([]RegenAttempt, error) { 103 log.Println("getting genproto changes") 104 genprotoPRs := []RegenAttempt{} 105 106 // We don't bother paginating, because it hurts our requests quota and makes 107 // the page slower without a lot of value. 108 opt := &github.PullRequestListOptions{ 109 ListOptions: github.ListOptions{PerPage: 50}, 110 State: "all", 111 } 112 prs, _, err := db.githubClient.PullRequests.List(ctx, "googleapis", "go-genproto", opt) 113 if err != nil { 114 return nil, err 115 } 116 for _, pr := range prs { 117 if !strings.Contains(pr.GetTitle(), "regen") { 118 continue 119 } 120 genprotoPRs = append(genprotoPRs, &GenprotoRegenAttempt{ 121 regenAttempt: regenAttempt{ 122 author: pr.GetUser().GetLogin(), 123 title: pr.GetTitle(), 124 url: pr.GetHTMLURL(), 125 created: pr.GetCreatedAt(), 126 open: pr.GetState() == "open", 127 }, 128 }) 129 } 130 return genprotoPRs, nil 131} 132 133// GetCLs fetches regen CLs from Gerrit. 134func (db *Db) GetCLs(ctx context.Context) ([]RegenAttempt, error) { 135 log.Println("getting gocloud changes") 136 gocloudCLs := []RegenAttempt{} 137 138 changes, _, err := db.gerritClient.Changes.QueryChanges(&gerrit.QueryChangeOptions{ 139 QueryOptions: gerrit.QueryOptions{Query: []string{"project:gocloud"}, Limit: 200}, 140 }) 141 if err != nil { 142 return nil, err 143 } 144 145 for _, c := range *changes { 146 if !strings.Contains(c.Subject, "regen") { 147 continue 148 } 149 150 // For some reason, the Changes API only returns AccountID. So now we 151 // have to go get the name. 152 db.cacheMu.Lock() 153 if _, ok := db.cachedGerritAccounts[c.Owner.AccountID]; !ok { 154 log.Println("looking up user", c.Owner.AccountID) 155 ai, resp, err := db.gerritClient.Accounts.GetAccount(strconv.Itoa(c.Owner.AccountID)) 156 if err != nil { 157 if resp.StatusCode == http.StatusNotFound { 158 db.cachedGerritAccounts[c.Owner.AccountID] = fmt.Sprintf("unknown user account ID: %d\n", c.Owner.AccountID) 159 } else { 160 db.cacheMu.Unlock() 161 return nil, err 162 } 163 } else { 164 db.cachedGerritAccounts[c.Owner.AccountID] = ai.Email 165 } 166 } 167 168 gocloudCLs = append(gocloudCLs, &GerritRegenAttempt{ 169 regenAttempt: regenAttempt{ 170 author: db.cachedGerritAccounts[c.Owner.AccountID], 171 title: c.Subject, 172 url: fmt.Sprintf("https://code-review.googlesource.com/q/%s", c.ChangeID), 173 created: c.Created.Time, 174 open: c.Status == "NEW", 175 }, 176 ChangeID: c.ChangeID, 177 }) 178 db.cacheMu.Unlock() 179 } 180 181 return gocloudCLs, nil 182} 183 184// FirstOpen returns the first open regen attempt. 185func FirstOpen(ras []RegenAttempt) (RegenAttempt, bool) { 186 for _, ra := range ras { 187 if ra.Open() { 188 return ra, true 189 } 190 } 191 return nil, false 192} 193