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  }