github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/tide/blockers/blockers.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package blockers 18 19 import ( 20 "context" 21 "fmt" 22 "regexp" 23 "sort" 24 "strings" 25 26 githubql "github.com/shurcooL/githubv4" 27 "github.com/sirupsen/logrus" 28 ) 29 30 var ( 31 branchRE = regexp.MustCompile(`(?im)\bbranch:[^\w-]*([\w-]+)\b`) 32 ) 33 34 type githubClient interface { 35 Query(context.Context, interface{}, map[string]interface{}) error 36 } 37 38 // Blocker specifies an issue number that should block tide from merging. 39 type Blocker struct { 40 Number int 41 Title, URL string 42 // TODO: time blocked? (when blocker label was added) 43 } 44 45 type orgRepo struct { 46 org, repo string 47 } 48 49 type orgRepoBranch struct { 50 org, repo, branch string 51 } 52 53 // Blockers holds maps of issues that are blocking various repos/branches. 54 type Blockers struct { 55 Repo map[orgRepo][]Blocker `json:"repo,omitempty"` 56 Branch map[orgRepoBranch][]Blocker `json:"branch,omitempty"` 57 } 58 59 // GetApplicable returns the subset of blockers applicable to the specified branch. 60 func (b Blockers) GetApplicable(org, repo, branch string) []Blocker { 61 var res []Blocker 62 res = append(res, b.Repo[orgRepo{org: org, repo: repo}]...) 63 res = append(res, b.Branch[orgRepoBranch{org: org, repo: repo, branch: branch}]...) 64 65 sort.Slice(res, func(i, j int) bool { 66 return res[i].Number < res[j].Number 67 }) 68 return res 69 } 70 71 // FindAll finds issues with label in the specified orgs/repos that should block tide. 72 func FindAll(ghc githubClient, log *logrus.Entry, label, orgRepoTokens string) (Blockers, error) { 73 issues, err := search( 74 context.Background(), 75 ghc, 76 log, 77 blockerQuery(label, orgRepoTokens), 78 ) 79 if err != nil { 80 return Blockers{}, fmt.Errorf("error searching for blocker issues: %v", err) 81 } 82 83 return fromIssues(issues), nil 84 } 85 86 func fromIssues(issues []Issue) Blockers { 87 res := Blockers{Repo: make(map[orgRepo][]Blocker), Branch: make(map[orgRepoBranch][]Blocker)} 88 for _, issue := range issues { 89 strippedTitle := branchRE.ReplaceAllLiteralString(string(issue.Title), "") 90 block := Blocker{ 91 Number: int(issue.Number), 92 Title: strippedTitle, 93 URL: string(issue.URL), 94 } 95 if branches := parseBranches(string(issue.Title)); len(branches) > 0 { 96 for _, branch := range branches { 97 key := orgRepoBranch{ 98 org: string(issue.Repository.Owner.Login), 99 repo: string(issue.Repository.Name), 100 branch: branch, 101 } 102 res.Branch[key] = append(res.Branch[key], block) 103 } 104 } else { 105 key := orgRepo{ 106 org: string(issue.Repository.Owner.Login), 107 repo: string(issue.Repository.Name), 108 } 109 res.Repo[key] = append(res.Repo[key], block) 110 } 111 } 112 return res 113 } 114 115 func blockerQuery(label, orgRepoTokens string) string { 116 tokens := []string{ 117 "is:issue", 118 "state:open", 119 fmt.Sprintf("label:\"%s\"", label), 120 orgRepoTokens, 121 } 122 return strings.Join(tokens, " ") 123 } 124 125 func parseBranches(str string) []string { 126 var res []string 127 for _, match := range branchRE.FindAllStringSubmatch(str, -1) { 128 res = append(res, match[1]) 129 } 130 return res 131 } 132 133 func search(ctx context.Context, ghc githubClient, log *logrus.Entry, q string) ([]Issue, error) { 134 var ret []Issue 135 vars := map[string]interface{}{ 136 "query": githubql.String(q), 137 "searchCursor": (*githubql.String)(nil), 138 } 139 var totalCost int 140 var remaining int 141 for { 142 sq := searchQuery{} 143 if err := ghc.Query(ctx, &sq, vars); err != nil { 144 return nil, err 145 } 146 totalCost += int(sq.RateLimit.Cost) 147 remaining = int(sq.RateLimit.Remaining) 148 for _, n := range sq.Search.Nodes { 149 ret = append(ret, n.Issue) 150 } 151 if !sq.Search.PageInfo.HasNextPage { 152 break 153 } 154 vars["searchCursor"] = githubql.NewString(sq.Search.PageInfo.EndCursor) 155 } 156 log.Debugf("Search for query \"%s\" cost %d point(s). %d remaining.", q, totalCost, remaining) 157 return ret, nil 158 } 159 160 // Issue holds graphql response data about issues 161 type Issue struct { 162 Number githubql.Int 163 Title githubql.String 164 URL githubql.String 165 Repository struct { 166 Name githubql.String 167 Owner struct { 168 Login githubql.String 169 } 170 } 171 } 172 173 type searchQuery struct { 174 RateLimit struct { 175 Cost githubql.Int 176 Remaining githubql.Int 177 } 178 Search struct { 179 PageInfo struct { 180 HasNextPage githubql.Boolean 181 EndCursor githubql.String 182 } 183 Nodes []struct { 184 Issue Issue `graphql:"... on Issue"` 185 } 186 } `graphql:"search(type: ISSUE, first: 100, after: $searchCursor, query: $query)"` 187 }