github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/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 "github.com/shurcooL/githubv4" 27 "github.com/sirupsen/logrus" 28 29 "k8s.io/apimachinery/pkg/util/sets" 30 ) 31 32 var ( 33 branchRE = regexp.MustCompile(`(?im)\bbranch:[^\w-]*([\w-]+)\b`) 34 ) 35 36 type githubClient interface { 37 Query(context.Context, interface{}, map[string]interface{}) error 38 } 39 40 // Blocker specifies an issue number that should block tide from merging. 41 type Blocker struct { 42 Number int 43 Title, URL string 44 // TODO: time blocked? (when blocker label was added) 45 } 46 47 type orgRepo struct { 48 org, repo string 49 } 50 51 type orgRepoBranch struct { 52 org, repo, branch string 53 } 54 55 // Blockers holds maps of issues that are blocking various repos/branches. 56 type Blockers struct { 57 Repo map[orgRepo][]Blocker `json:"repo,omitempty"` 58 Branch map[orgRepoBranch][]Blocker `json:"branch,omitempty"` 59 } 60 61 // GetApplicable returns the subset of blockers applicable to the specified branch. 62 func (b Blockers) GetApplicable(org, repo, branch string) []Blocker { 63 var res []Blocker 64 res = append(res, b.Repo[orgRepo{org: org, repo: repo}]...) 65 res = append(res, b.Branch[orgRepoBranch{org: org, repo: repo, branch: branch}]...) 66 67 sort.Slice(res, func(i, j int) bool { 68 return res[i].Number < res[j].Number 69 }) 70 return res 71 } 72 73 // FindAll finds issues with label in the specified orgs/repos that should block tide. 74 func FindAll(ghc githubClient, log *logrus.Entry, label string, orgs, repos sets.String) (Blockers, error) { 75 issues, err := search( 76 context.Background(), 77 ghc, 78 log, 79 blockerQuery(label, orgs, repos), 80 ) 81 if err != nil { 82 return Blockers{}, fmt.Errorf("error searching for blocker issues: %v", err) 83 } 84 85 return fromIssues(issues), nil 86 } 87 88 func fromIssues(issues []Issue) Blockers { 89 res := Blockers{Repo: make(map[orgRepo][]Blocker), Branch: make(map[orgRepoBranch][]Blocker)} 90 for _, issue := range issues { 91 strippedTitle := branchRE.ReplaceAllLiteralString(string(issue.Title), "") 92 block := Blocker{ 93 Number: int(issue.Number), 94 Title: strippedTitle, 95 URL: string(issue.HTMLURL), 96 } 97 if branches := parseBranches(string(issue.Title)); len(branches) > 0 { 98 for _, branch := range branches { 99 key := orgRepoBranch{ 100 org: string(issue.Repository.Owner.Login), 101 repo: string(issue.Repository.Name), 102 branch: branch, 103 } 104 res.Branch[key] = append(res.Branch[key], block) 105 } 106 } else { 107 key := orgRepo{ 108 org: string(issue.Repository.Owner.Login), 109 repo: string(issue.Repository.Name), 110 } 111 res.Repo[key] = append(res.Repo[key], block) 112 } 113 } 114 return res 115 } 116 117 func blockerQuery(label string, orgs, repos sets.String) string { 118 tokens := []string{"is:issue", "state:open", fmt.Sprintf("label:\"%s\"", label)} 119 for _, org := range orgs.List() { 120 tokens = append(tokens, fmt.Sprintf("org:\"%s\"", org)) 121 } 122 for _, repo := range repos.List() { 123 tokens = append(tokens, fmt.Sprintf("repo:\"%s\"", repo)) 124 } 125 return strings.Join(tokens, " ") 126 } 127 128 func parseBranches(str string) []string { 129 var res []string 130 for _, match := range branchRE.FindAllStringSubmatch(str, -1) { 131 res = append(res, match[1]) 132 } 133 return res 134 } 135 136 func search(ctx context.Context, ghc githubClient, log *logrus.Entry, q string) ([]Issue, error) { 137 var ret []Issue 138 vars := map[string]interface{}{ 139 "query": githubv4.String(q), 140 "searchCursor": (*githubv4.String)(nil), 141 } 142 var totalCost int 143 var remaining int 144 for { 145 sq := searchQuery{} 146 if err := ghc.Query(ctx, &sq, vars); err != nil { 147 return nil, err 148 } 149 totalCost += int(sq.RateLimit.Cost) 150 remaining = int(sq.RateLimit.Remaining) 151 for _, n := range sq.Search.Nodes { 152 ret = append(ret, n.Issue) 153 } 154 if !sq.Search.PageInfo.HasNextPage { 155 break 156 } 157 vars["searchCursor"] = githubv4.NewString(sq.Search.PageInfo.EndCursor) 158 } 159 log.Debugf("Search for query \"%s\" cost %d point(s). %d remaining.", q, totalCost, remaining) 160 return ret, nil 161 } 162 163 // Issue holds graphql response data about issues 164 // TODO: validate that fields are populated properly 165 type Issue struct { 166 Number githubv4.Int 167 Title githubv4.String 168 HTMLURL githubv4.String 169 Repository struct { 170 Name githubv4.String 171 Owner struct { 172 Login githubv4.String 173 } 174 } 175 } 176 177 type searchQuery struct { 178 RateLimit struct { 179 Cost githubv4.Int 180 Remaining githubv4.Int 181 } 182 Search struct { 183 PageInfo struct { 184 HasNextPage githubv4.Boolean 185 EndCursor githubv4.String 186 } 187 Nodes []struct { 188 Issue Issue `graphql:"... on Issue"` 189 } 190 } `graphql:"search(type: ISSUE, first: 100, after: $searchCursor, query: $query)"` 191 }