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  }