sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/tide.go (about)

     1  /*
     2  Copyright 2017 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 config
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  	"sync"
    27  	"text/template"
    28  
    29  	"github.com/sirupsen/logrus"
    30  
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  	"sigs.k8s.io/prow/pkg/git/types"
    35  	"sigs.k8s.io/prow/pkg/git/v2"
    36  )
    37  
    38  // TideQueries is a TideQuery slice.
    39  type TideQueries []TideQuery
    40  
    41  type TideBranchMergeType struct {
    42  	MergeType types.PullRequestMergeType
    43  	Regexpr   *regexp.Regexp
    44  }
    45  
    46  func (tbmt TideBranchMergeType) Match(branch string) bool {
    47  	return tbmt.Regexpr.MatchString(branch)
    48  }
    49  
    50  func (tbmt TideBranchMergeType) MarshalJSON() ([]byte, error) {
    51  	return json.Marshal(tbmt.MergeType)
    52  }
    53  
    54  func (tbmt *TideBranchMergeType) UnmarshalJSON(b []byte) error {
    55  	return json.Unmarshal(b, &tbmt.MergeType)
    56  }
    57  
    58  type TideRepoMergeType struct {
    59  	Branches  map[string]TideBranchMergeType
    60  	MergeType types.PullRequestMergeType
    61  }
    62  
    63  // When TideRepoMergeType.MergeType is present, unmarshal into:
    64  //
    65  //	kubernetes: squash
    66  //
    67  // when TideRepoMergeType.Branches is not empty, unmarshal into:
    68  //
    69  //	kubernetes:
    70  //	  main: squash
    71  func (trmt TideRepoMergeType) MarshalJSON() ([]byte, error) {
    72  	if trmt.MergeType != "" {
    73  		return json.Marshal(trmt.MergeType)
    74  	}
    75  	if trmt.Branches == nil || len(trmt.Branches) == 0 {
    76  		return json.Marshal("")
    77  	}
    78  	return json.Marshal(trmt.Branches)
    79  }
    80  
    81  // Full configuration:
    82  //
    83  //	test-infra:
    84  //	  main: merge
    85  //
    86  // unmarshal into map[string][TideBranchMergeType]
    87  //
    88  // Repo-wide configuration:
    89  //
    90  //	test-infra: merge
    91  //
    92  // unmarshal into types.PullRequestMergeType
    93  func (trmt *TideRepoMergeType) UnmarshalJSON(b []byte) error {
    94  	var mt types.PullRequestMergeType
    95  	if err := json.Unmarshal(b, &mt); err == nil {
    96  		trmt.MergeType = mt
    97  		return nil
    98  	}
    99  	var branches map[string]TideBranchMergeType
   100  	if err := json.Unmarshal(b, &branches); err != nil {
   101  		return err
   102  	}
   103  	trmt.Branches = branches
   104  	return nil
   105  }
   106  
   107  type TideOrgMergeType struct {
   108  	Repos     map[string]TideRepoMergeType
   109  	MergeType types.PullRequestMergeType
   110  }
   111  
   112  // When TideOrgMergeType.MergeType is present, unmarshal into:
   113  //
   114  //	kubernetes: squash
   115  //
   116  // when TideOrgMergeType.Repos is not empty, unmarshal into:
   117  //
   118  //	kubernetes:
   119  //	  test-infra: squash
   120  func (tomt TideOrgMergeType) MarshalJSON() ([]byte, error) {
   121  	if tomt.MergeType != "" {
   122  		return json.Marshal(tomt.MergeType)
   123  	}
   124  	if tomt.Repos == nil || len(tomt.Repos) == 0 {
   125  		return json.Marshal("")
   126  	}
   127  	return json.Marshal(tomt.Repos)
   128  }
   129  
   130  // Org-wide configuration:
   131  //
   132  //	kubernetes: merge
   133  //
   134  // unmarshal into types.PullRequestMergeType.
   135  //
   136  // Full configuration:
   137  //
   138  //	kubernetes:
   139  //	  test-infra:
   140  //	    main: merge
   141  //
   142  // unmarshal into map[string][TideRepoMergeType]:
   143  func (tomt *TideOrgMergeType) UnmarshalJSON(b []byte) error {
   144  	var mt types.PullRequestMergeType
   145  	if err := json.Unmarshal(b, &mt); err == nil {
   146  		tomt.MergeType = mt
   147  		return nil
   148  	}
   149  	var repos map[string]TideRepoMergeType
   150  	if err := json.Unmarshal(b, &repos); err != nil {
   151  		return err
   152  	}
   153  	tomt.Repos = repos
   154  	return nil
   155  }
   156  
   157  // TideContextPolicy configures options about how to handle various contexts.
   158  type TideContextPolicy struct {
   159  	// whether to consider unknown contexts optional (skip) or required.
   160  	SkipUnknownContexts       *bool    `json:"skip-unknown-contexts,omitempty"`
   161  	RequiredContexts          []string `json:"required-contexts,omitempty"`
   162  	RequiredIfPresentContexts []string `json:"required-if-present-contexts,omitempty"`
   163  	OptionalContexts          []string `json:"optional-contexts,omitempty"`
   164  	// Infer required and optional jobs from Branch Protection configuration
   165  	FromBranchProtection *bool `json:"from-branch-protection,omitempty"`
   166  }
   167  
   168  // TideOrgContextPolicy overrides the policy for an org, and any repo overrides.
   169  type TideOrgContextPolicy struct {
   170  	TideContextPolicy `json:",inline"`
   171  	Repos             map[string]TideRepoContextPolicy `json:"repos,omitempty"`
   172  }
   173  
   174  // TideRepoContextPolicy overrides the policy for repo, and any branch overrides.
   175  type TideRepoContextPolicy struct {
   176  	TideContextPolicy `json:",inline"`
   177  	Branches          map[string]TideContextPolicy `json:"branches,omitempty"`
   178  }
   179  
   180  // TideContextPolicyOptions holds the default policy, and any org overrides.
   181  type TideContextPolicyOptions struct {
   182  	TideContextPolicy `json:",inline"`
   183  	// GitHub Orgs
   184  	Orgs map[string]TideOrgContextPolicy `json:"orgs,omitempty"`
   185  }
   186  
   187  // TideMergeCommitTemplate holds templates to use for merge commits.
   188  type TideMergeCommitTemplate struct {
   189  	TitleTemplate string `json:"title,omitempty"`
   190  	BodyTemplate  string `json:"body,omitempty"`
   191  
   192  	Title *template.Template `json:"-"`
   193  	Body  *template.Template `json:"-"`
   194  }
   195  
   196  // TidePriority contains a list of labels used to prioritize PRs in the merge pool
   197  type TidePriority struct {
   198  	Labels []string `json:"labels,omitempty"`
   199  }
   200  
   201  // Tide is config for the tide pool.
   202  type Tide struct {
   203  	Gerrit *TideGerritConfig `json:"gerrit,omitempty"`
   204  	// SyncPeriod specifies how often Tide will sync jobs with GitHub. Defaults to 1m.
   205  	SyncPeriod *metav1.Duration `json:"sync_period,omitempty"`
   206  	// MaxGoroutines is the maximum number of goroutines spawned inside the
   207  	// controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
   208  	// positive number.
   209  	MaxGoroutines int `json:"max_goroutines,omitempty"`
   210  	// BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and
   211  	// integer batch size limit as the value. Use "*" as key to set a global default.
   212  	// Special values:
   213  	//  0 => unlimited batch size
   214  	// -1 => batch merging disabled :(
   215  	BatchSizeLimitMap map[string]int `json:"batch_size_limit,omitempty"`
   216  	// PrioritizeExistingBatches configures on org or org/repo level if Tide should continue
   217  	// testing pre-existing batches instead of immediately including new PRs as they become
   218  	// eligible. Continuing on an old batch allows to re-use all existing test results whereas
   219  	// starting a new one requires to start new instances of all tests.
   220  	// Use '*' as key to set this globally. Defaults to true.
   221  	PrioritizeExistingBatchesMap map[string]bool `json:"prioritize_existing_batches,omitempty"`
   222  
   223  	TideGitHubConfig `json:",inline"`
   224  }
   225  
   226  // TideGitHubConfig is the tide config for GitHub.
   227  type TideGitHubConfig struct {
   228  	// StatusUpdatePeriod specifies how often Tide will update GitHub status contexts.
   229  	// Defaults to the value of SyncPeriod.
   230  	StatusUpdatePeriod *metav1.Duration `json:"status_update_period,omitempty"`
   231  	// Queries represents a list of GitHub search queries that collectively
   232  	// specify the set of PRs that meet merge requirements.
   233  	Queries TideQueries `json:"queries,omitempty"`
   234  
   235  	// A key/value pair of an org/repo as the key and merge method to override
   236  	// the default method of merge. Valid options are squash, rebase, and merge.
   237  	MergeType map[string]TideOrgMergeType `json:"merge_method,omitempty"`
   238  
   239  	// A key/value pair of an org/repo as the key and Go template to override
   240  	// the default merge commit title and/or message. Template is passed the
   241  	// PullRequest struct (prow/github/types.go#PullRequest)
   242  	MergeTemplate map[string]TideMergeCommitTemplate `json:"merge_commit_template,omitempty"`
   243  
   244  	// URL for tide status contexts.
   245  	// We can consider allowing this to be set separately for separate repos, or
   246  	// allowing it to be a template.
   247  	TargetURL string `json:"target_url,omitempty"`
   248  
   249  	// TargetURLs is a map from "*", <org>, or <org/repo> to the URL for the tide status contexts.
   250  	// The most specific key that matches will be used.
   251  	// This field is mutually exclusive with TargetURL.
   252  	TargetURLs map[string]string `json:"target_urls,omitempty"`
   253  
   254  	// PRStatusBaseURL is the base URL for the PR status page.
   255  	// This is used to link to a merge requirements overview
   256  	// in the tide status context.
   257  	// Will be deprecated on June 2020.
   258  	PRStatusBaseURL string `json:"pr_status_base_url,omitempty"`
   259  
   260  	// PRStatusBaseURLs is the base URL for the PR status page
   261  	// mapped by org or org/repo level.
   262  	PRStatusBaseURLs map[string]string `json:"pr_status_base_urls,omitempty"`
   263  
   264  	// BlockerLabel is an optional label that is used to identify merge blocking
   265  	// GitHub issues.
   266  	// Leave this blank to disable this feature and save 1 API token per sync loop.
   267  	BlockerLabel string `json:"blocker_label,omitempty"`
   268  
   269  	// SquashLabel is an optional label that is used to identify PRs that should
   270  	// always be squash merged.
   271  	// Leave this blank to disable this feature.
   272  	SquashLabel string `json:"squash_label,omitempty"`
   273  
   274  	// RebaseLabel is an optional label that is used to identify PRs that should
   275  	// always be rebased and merged.
   276  	// Leave this blank to disable this feature.
   277  	RebaseLabel string `json:"rebase_label,omitempty"`
   278  
   279  	// MergeLabel is an optional label that is used to identify PRs that should
   280  	// always be merged with all individual commits from the PR.
   281  	// Leave this blank to disable this feature.
   282  	MergeLabel string `json:"merge_label,omitempty"`
   283  
   284  	// TideContextPolicyOptions defines merge options for context. If not set it will infer
   285  	// the required and optional contexts from the prow jobs configured and use the github
   286  	// combined status; otherwise it may apply the branch protection setting or let user
   287  	// define their own options in case branch protection is not used.
   288  	ContextOptions TideContextPolicyOptions `json:"context_options,omitempty"`
   289  
   290  	// BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and
   291  	// integer batch size limit as the value. Use "*" as key to set a global default.
   292  	// Special values:
   293  	//  0 => unlimited batch size
   294  	// -1 => batch merging disabled :(
   295  	BatchSizeLimitMap map[string]int `json:"batch_size_limit,omitempty"`
   296  
   297  	// Priority is an ordered list of sets of labels that would be prioritized before other PRs
   298  	// PRs should match all labels contained in a set to be prioritized. The first entry has
   299  	// the highest priority.
   300  	Priority []TidePriority `json:"priority,omitempty"`
   301  
   302  	// DisplayAllQueriesInStatus controls if Tide should mention all queries in the status it
   303  	// creates. The default is to only mention the one to which we are closest (Calculated
   304  	// by total number of requirements - fulfilled number of requirements).
   305  	DisplayAllQueriesInStatus bool `json:"display_all_tide_queries_in_status,omitempty"`
   306  }
   307  
   308  // TideGerritConfig contains all Gerrit related configurations for tide.
   309  type TideGerritConfig struct {
   310  	Queries GerritOrgRepoConfigs `json:"queries"`
   311  	// RateLimit defines how many changes to query per gerrit API call
   312  	// default is 5.
   313  	RateLimit int `json:"ratelimit,omitempty"`
   314  }
   315  
   316  func (t *Tide) mergeFrom(additional *Tide) error {
   317  
   318  	// Duplicate queries are pointless but not harmful, we
   319  	// have code to de-duplicate them down the line to not
   320  	// increase token usage needlessly.
   321  	t.Queries = append(t.Queries, additional.Queries...)
   322  
   323  	if t.MergeType == nil {
   324  		t.MergeType = additional.MergeType
   325  		return nil
   326  	}
   327  
   328  	var errs []error
   329  	for orgOrRepo, mergeMethod := range additional.MergeType {
   330  		if _, alreadyConfigured := t.MergeType[orgOrRepo]; alreadyConfigured {
   331  			errs = append(errs, fmt.Errorf("config for org or repo %s passed more than once", orgOrRepo))
   332  			continue
   333  		}
   334  		t.MergeType[orgOrRepo] = mergeMethod
   335  	}
   336  
   337  	return utilerrors.NewAggregate(errs)
   338  }
   339  
   340  func (t *Tide) PrioritizeExistingBatches(repo OrgRepo) bool {
   341  	if val, set := t.PrioritizeExistingBatchesMap[repo.String()]; set {
   342  		return val
   343  	}
   344  	if val, set := t.PrioritizeExistingBatchesMap[repo.Org]; set {
   345  		return val
   346  	}
   347  
   348  	if val, set := t.PrioritizeExistingBatchesMap["*"]; set {
   349  		return val
   350  	}
   351  
   352  	return true
   353  }
   354  
   355  func (t *Tide) BatchSizeLimit(repo OrgRepo) int {
   356  	if limit, ok := t.BatchSizeLimitMap[repo.String()]; ok {
   357  		return limit
   358  	}
   359  	if limit, ok := t.BatchSizeLimitMap[repo.Org]; ok {
   360  		return limit
   361  	}
   362  	return t.BatchSizeLimitMap["*"]
   363  }
   364  
   365  // MergeMethod returns the merge method to use for a repo. The default of merge is
   366  // returned when not overridden.
   367  func (t *Tide) MergeMethod(repo OrgRepo) types.PullRequestMergeType {
   368  	return t.OrgRepoBranchMergeMethod(repo, "")
   369  }
   370  
   371  // OrgRepoBranchMergeMethod returns the merge method to use for a given triple: org, repo, branch.
   372  // The following matching criteria apply, the priority goes from the highest to the lowest:
   373  //
   374  //  1. kubernetes/test-infra@main: rebase       org/repo@branch shorthand
   375  //
   376  //  2. kubernetes:
   377  //     test-infra:
   378  //     ma(ster|in): rebase                  branch level regex
   379  //
   380  //  3. kubernetes/test-infra: rebase            org/repo shorthand
   381  //
   382  //  4. kubernetes:
   383  //     test-infra: rebase                     repo-wide config
   384  //
   385  //  5. kubernetes: rebase                       org shorthand
   386  //
   387  //  6. default to "merge"
   388  func (t *Tide) OrgRepoBranchMergeMethod(orgRepo OrgRepo, branch string) types.PullRequestMergeType {
   389  	isOrgSet, isRepoSet, isBranchSet := orgRepo.Org != "", orgRepo.Repo != "", branch != ""
   390  	var orgFound, repoFound bool
   391  
   392  	// The repository to look for can either be provided as an input or the "*" wildcard
   393  	repo := orgRepo.Repo
   394  
   395  	// Check if the org exists
   396  	if isOrgSet {
   397  		_, orgFound = t.MergeType[orgRepo.Org]
   398  	}
   399  
   400  	// Check if the repo exists
   401  	if isOrgSet && isRepoSet && orgFound {
   402  		_, repoFound = t.MergeType[orgRepo.Org].Repos[orgRepo.Repo]
   403  		_, wildcardRepoFound := t.MergeType[orgRepo.Org].Repos["*"]
   404  		if !repoFound && wildcardRepoFound {
   405  			repoFound = true
   406  			repo = "*"
   407  		}
   408  	}
   409  
   410  	// 1. "$org/$repo@$branch" shorthand
   411  	if isOrgSet && isRepoSet && isBranchSet {
   412  		orgRepoBranchShorthand := fmt.Sprintf("%s/%s@%s", orgRepo.Org, orgRepo.Repo, branch)
   413  		if orgRepoBranch, found := t.MergeType[orgRepoBranchShorthand]; found && orgRepoBranch.MergeType != "" {
   414  			return orgRepoBranch.MergeType
   415  		}
   416  	}
   417  
   418  	// 2. Branch level regex match
   419  	if orgFound && repoFound {
   420  		branches := t.MergeType[orgRepo.Org].Repos[repo].Branches
   421  		keys := make([]string, 0, len(branches))
   422  
   423  		for k := range branches {
   424  			keys = append(keys, k)
   425  		}
   426  		sort.Strings(keys)
   427  
   428  		for _, key := range keys {
   429  			branchConfig := branches[key]
   430  			if branchConfig.Regexpr.MatchString(branch) {
   431  				return branchConfig.MergeType
   432  			}
   433  		}
   434  	}
   435  
   436  	// 3. "$org/$repo" shorthand
   437  	if isOrgSet && isRepoSet {
   438  		orgRepoShorthand := fmt.Sprintf("%s/%s", orgRepo.Org, orgRepo.Repo)
   439  		if orgRepo, found := t.MergeType[orgRepoShorthand]; found && orgRepo.MergeType != "" {
   440  			return orgRepo.MergeType
   441  		}
   442  	}
   443  
   444  	// 4. Repo-wide match
   445  	if orgFound && repoFound {
   446  		if t.MergeType[orgRepo.Org].Repos[repo].MergeType != "" {
   447  			return t.MergeType[orgRepo.Org].Repos[repo].MergeType
   448  		}
   449  	}
   450  
   451  	// 5. "$org" shorthand
   452  	if orgFound {
   453  		if t.MergeType[orgRepo.Org].MergeType != "" {
   454  			return t.MergeType[orgRepo.Org].MergeType
   455  		}
   456  	}
   457  
   458  	// 6. Default
   459  	return types.MergeMerge
   460  }
   461  
   462  // MergeCommitTemplate returns a struct with Go template string(s) or nil
   463  func (t *Tide) MergeCommitTemplate(repo OrgRepo) TideMergeCommitTemplate {
   464  	v, ok := t.MergeTemplate[repo.String()]
   465  	if !ok {
   466  		return t.MergeTemplate[repo.Org]
   467  	}
   468  
   469  	return v
   470  }
   471  
   472  func (t *Tide) GetPRStatusBaseURL(repo OrgRepo) string {
   473  	if byOrgRepo, ok := t.PRStatusBaseURLs[repo.String()]; ok {
   474  		return byOrgRepo
   475  	}
   476  	if byOrg, ok := t.PRStatusBaseURLs[repo.Org]; ok {
   477  		return byOrg
   478  	}
   479  
   480  	return t.PRStatusBaseURLs["*"]
   481  }
   482  
   483  func (t *Tide) GetTargetURL(repo OrgRepo) string {
   484  	if byOrgRepo, ok := t.TargetURLs[repo.String()]; ok {
   485  		return byOrgRepo
   486  	}
   487  	if byOrg, ok := t.TargetURLs[repo.Org]; ok {
   488  		return byOrg
   489  	}
   490  
   491  	return t.TargetURLs["*"]
   492  }
   493  
   494  // TideQuery is turned into a GitHub search query. See the docs for details:
   495  // https://help.github.com/articles/searching-issues-and-pull-requests/
   496  type TideQuery struct {
   497  	Author string `json:"author,omitempty"`
   498  
   499  	Labels        []string `json:"labels,omitempty"`
   500  	MissingLabels []string `json:"missingLabels,omitempty"`
   501  
   502  	ExcludedBranches []string `json:"excludedBranches,omitempty"`
   503  	IncludedBranches []string `json:"includedBranches,omitempty"`
   504  
   505  	Milestone string `json:"milestone,omitempty"`
   506  
   507  	ReviewApprovedRequired bool `json:"reviewApprovedRequired,omitempty"`
   508  
   509  	Orgs          []string `json:"orgs,omitempty"`
   510  	Repos         []string `json:"repos,omitempty"`
   511  	ExcludedRepos []string `json:"excludedRepos,omitempty"`
   512  }
   513  
   514  func (q TideQuery) TenantIDs(cfg Config) []string {
   515  	res := sets.Set[string]{}
   516  	for _, org := range q.Orgs {
   517  		res.Insert(cfg.GetProwJobDefault(org, "*").TenantID)
   518  	}
   519  	for _, repo := range q.Repos {
   520  		res.Insert(cfg.GetProwJobDefault(repo, "*").TenantID)
   521  	}
   522  	return sets.List(res)
   523  }
   524  
   525  // tideQueryConfig contains the subset of attributes by which we de-duplicate
   526  // tide queries. Together with tideQueryTarget it must contain the full set
   527  // of all TideQuery properties.
   528  type tideQueryConfig struct {
   529  	Author                 string
   530  	ExcludedBranches       []string
   531  	IncludedBranches       []string
   532  	Labels                 []string
   533  	MissingLabels          []string
   534  	Milestone              string
   535  	ReviewApprovedRequired bool
   536  	TenantIDs              []string
   537  }
   538  
   539  type tideQueryTarget struct {
   540  	Orgs          []string
   541  	Repos         []string
   542  	ExcludedRepos []string
   543  }
   544  
   545  // constructQuery returns a map[org][]orgSpecificQueryParts (org, repo, -repo), remainingQueryString
   546  func (tq *TideQuery) constructQuery() (map[string][]string, string) {
   547  	// map org->repo directives (if any)
   548  	orgScopedIdentifiers := map[string][]string{}
   549  	for _, o := range tq.Orgs {
   550  		if _, ok := orgScopedIdentifiers[o]; !ok {
   551  			orgScopedIdentifiers[o] = []string{fmt.Sprintf(`org:"%s"`, o)}
   552  		}
   553  	}
   554  	for _, r := range tq.Repos {
   555  		if org, _, ok := splitOrgRepoString(r); ok {
   556  			orgScopedIdentifiers[org] = append(orgScopedIdentifiers[org], fmt.Sprintf("repo:\"%s\"", r))
   557  		}
   558  	}
   559  
   560  	for _, r := range tq.ExcludedRepos {
   561  		if org, _, ok := splitOrgRepoString(r); ok {
   562  			orgScopedIdentifiers[org] = append(orgScopedIdentifiers[org], fmt.Sprintf("-repo:\"%s\"", r))
   563  		}
   564  	}
   565  
   566  	queryString := []string{"is:pr", "state:open", "archived:false"}
   567  	if tq.Author != "" {
   568  		queryString = append(queryString, fmt.Sprintf("author:\"%s\"", tq.Author))
   569  	}
   570  	for _, b := range tq.ExcludedBranches {
   571  		queryString = append(queryString, fmt.Sprintf("-base:\"%s\"", b))
   572  	}
   573  	for _, b := range tq.IncludedBranches {
   574  		queryString = append(queryString, fmt.Sprintf("base:\"%s\"", b))
   575  	}
   576  	for _, l := range tq.Labels {
   577  		var orOperands []string
   578  		for _, alt := range strings.Split(l, ",") {
   579  			orOperands = append(orOperands, fmt.Sprintf("\"%s\"", alt))
   580  		}
   581  		queryString = append(queryString, fmt.Sprintf("label:%s", strings.Join(orOperands, ",")))
   582  	}
   583  	for _, l := range tq.MissingLabels {
   584  		queryString = append(queryString, fmt.Sprintf("-label:\"%s\"", l))
   585  	}
   586  	if tq.Milestone != "" {
   587  		queryString = append(queryString, fmt.Sprintf("milestone:\"%s\"", tq.Milestone))
   588  	}
   589  	if tq.ReviewApprovedRequired {
   590  		queryString = append(queryString, "review:approved")
   591  	}
   592  
   593  	return orgScopedIdentifiers, strings.Join(queryString, " ")
   594  }
   595  
   596  func splitOrgRepoString(orgRepo string) (string, string, bool) {
   597  	split := strings.Split(orgRepo, "/")
   598  	if len(split) != 2 {
   599  		// Just do it like the github search itself and ignore invalid orgRepo identifiers
   600  		return "", "", false
   601  	}
   602  	return split[0], split[1], true
   603  }
   604  
   605  // OrgQueries returns the GitHub search string for the query, sharded
   606  // by org.
   607  func (tq *TideQuery) OrgQueries() map[string]string {
   608  	orgRepoIdentifiers, queryString := tq.constructQuery()
   609  	result := map[string]string{}
   610  	for org, repoIdentifiers := range orgRepoIdentifiers {
   611  		result[org] = queryString + " " + strings.Join(repoIdentifiers, " ")
   612  	}
   613  
   614  	return result
   615  }
   616  
   617  // Query returns the corresponding github search string for the tide query.
   618  func (tq *TideQuery) Query() string {
   619  	orgRepoIdentifiers, queryString := tq.constructQuery()
   620  	toks := []string{queryString}
   621  	for _, repoIdentifiers := range orgRepoIdentifiers {
   622  		toks = append(toks, repoIdentifiers...)
   623  	}
   624  	return strings.Join(toks, " ")
   625  }
   626  
   627  // ForRepo indicates if the tide query applies to the specified repo.
   628  func (tq TideQuery) ForRepo(repo OrgRepo) bool {
   629  	for _, queryOrg := range tq.Orgs {
   630  		if queryOrg != repo.Org {
   631  			continue
   632  		}
   633  		// Check for repos excluded from the org.
   634  		for _, excludedRepo := range tq.ExcludedRepos {
   635  			if excludedRepo == repo.String() {
   636  				return false
   637  			}
   638  		}
   639  		return true
   640  	}
   641  	for _, queryRepo := range tq.Repos {
   642  		if queryRepo == repo.String() {
   643  			return true
   644  		}
   645  	}
   646  	return false
   647  }
   648  
   649  func reposInOrg(org string, repos []string) []string {
   650  	prefix := org + "/"
   651  	var res []string
   652  	for _, repo := range repos {
   653  		if strings.HasPrefix(repo, prefix) {
   654  			res = append(res, repo)
   655  		}
   656  	}
   657  	return res
   658  }
   659  
   660  // OrgExceptionsAndRepos determines which orgs and repos a set of queries cover.
   661  // Output is returned as a mapping from 'included org'->'repos excluded in the org'
   662  // and a set of included repos.
   663  func (tqs TideQueries) OrgExceptionsAndRepos() (map[string]sets.Set[string], sets.Set[string]) {
   664  	orgs := make(map[string]sets.Set[string])
   665  	for i := range tqs {
   666  		for _, org := range tqs[i].Orgs {
   667  			applicableRepos := sets.New[string](reposInOrg(org, tqs[i].ExcludedRepos)...)
   668  			if excepts, ok := orgs[org]; !ok {
   669  				// We have not seen this org so the exceptions are just applicable
   670  				// members of 'excludedRepos'.
   671  				orgs[org] = applicableRepos
   672  			} else {
   673  				// We have seen this org so the exceptions are the applicable
   674  				// members of 'excludedRepos' intersected with existing exceptions.
   675  				orgs[org] = excepts.Intersection(applicableRepos)
   676  			}
   677  		}
   678  	}
   679  	repos := sets.New[string]()
   680  	for i := range tqs {
   681  		repos.Insert(tqs[i].Repos...)
   682  	}
   683  	// Remove any org exceptions that are explicitly included in a different query.
   684  	reposList := repos.UnsortedList()
   685  	for _, excepts := range orgs {
   686  		excepts.Delete(reposList...)
   687  	}
   688  	return orgs, repos
   689  }
   690  
   691  // QueryMap is a struct mapping from "org/repo" -> TideQueries that
   692  // apply to that org or repo. It is lazily populated, but threadsafe.
   693  type QueryMap struct {
   694  	queries TideQueries
   695  
   696  	cache map[string]TideQueries
   697  	sync.Mutex
   698  }
   699  
   700  // QueryMap creates a QueryMap from TideQueries
   701  func (tqs TideQueries) QueryMap() *QueryMap {
   702  	return &QueryMap{
   703  		queries: tqs,
   704  		cache:   make(map[string]TideQueries),
   705  	}
   706  }
   707  
   708  // ForRepo returns the tide queries that apply to a repo.
   709  func (qm *QueryMap) ForRepo(repo OrgRepo) TideQueries {
   710  	res := TideQueries(nil)
   711  
   712  	qm.Lock()
   713  	defer qm.Unlock()
   714  
   715  	if qs, ok := qm.cache[repo.String()]; ok {
   716  		return append(res, qs...) // Return a copy.
   717  	}
   718  	// Cache miss. Need to determine relevant queries.
   719  
   720  	for _, query := range qm.queries {
   721  		if query.ForRepo(repo) {
   722  			res = append(res, query)
   723  		}
   724  	}
   725  	qm.cache[repo.String()] = res
   726  	return res
   727  }
   728  
   729  // Validate returns an error if the query has any errors.
   730  //
   731  // Examples include:
   732  // * an org name that is empty or includes a /
   733  // * repos that are not org/repo
   734  // * a label that is in both the labels and missing_labels section
   735  // * a branch that is in both included and excluded branch set.
   736  func (tq *TideQuery) Validate() error {
   737  	duplicates := func(field string, list []string) error {
   738  		dups := sets.New[string]()
   739  		seen := sets.New[string]()
   740  		for _, elem := range list {
   741  			if seen.Has(elem) {
   742  				dups.Insert(elem)
   743  			} else {
   744  				seen.Insert(elem)
   745  			}
   746  		}
   747  		dupCount := len(list) - seen.Len()
   748  		if dupCount == 0 {
   749  			return nil
   750  		}
   751  		return fmt.Errorf("%q contains %d duplicate entries: %s", field, dupCount, strings.Join(sets.List(dups), ", "))
   752  	}
   753  
   754  	orgs := sets.New[string]()
   755  	for o := range tq.Orgs {
   756  		if strings.Contains(tq.Orgs[o], "/") {
   757  			return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o])
   758  		}
   759  		if len(tq.Orgs[o]) == 0 {
   760  			return fmt.Errorf("orgs[%d]: is an empty string", o)
   761  		}
   762  		orgs.Insert(tq.Orgs[o])
   763  	}
   764  	if err := duplicates("orgs", tq.Orgs); err != nil {
   765  		return err
   766  	}
   767  
   768  	for r := range tq.Repos {
   769  		parts := strings.Split(tq.Repos[r], "/")
   770  		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
   771  			return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r])
   772  		}
   773  		if orgs.Has(parts[0]) {
   774  			return fmt.Errorf("repos[%d]: %q is already included via org: %q", r, tq.Repos[r], parts[0])
   775  		}
   776  	}
   777  	if err := duplicates("repos", tq.Repos); err != nil {
   778  		return err
   779  	}
   780  
   781  	if len(tq.Orgs) == 0 && len(tq.Repos) == 0 {
   782  		return errors.New("'orgs' and 'repos' cannot both be empty")
   783  	}
   784  
   785  	for er := range tq.ExcludedRepos {
   786  		parts := strings.Split(tq.ExcludedRepos[er], "/")
   787  		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
   788  			return fmt.Errorf("excludedRepos[%d]: %q is not of the form \"org/repo\"", er, tq.ExcludedRepos[er])
   789  		}
   790  		if !orgs.Has(parts[0]) {
   791  			return fmt.Errorf("excludedRepos[%d]: %q has no effect because org %q is not included", er, tq.ExcludedRepos[er], parts[0])
   792  		}
   793  		// Note: At this point we also know that this excludedRepo is not found in 'repos'.
   794  	}
   795  	if err := duplicates("excludedRepos", tq.ExcludedRepos); err != nil {
   796  		return err
   797  	}
   798  
   799  	if invalids := sets.New[string](tq.Labels...).Intersection(sets.New[string](tq.MissingLabels...)); len(invalids) > 0 {
   800  		return fmt.Errorf("the labels: %q are both required and forbidden", sets.List(invalids))
   801  	}
   802  	if err := duplicates("labels", tq.Labels); err != nil {
   803  		return err
   804  	}
   805  	if err := duplicates("missingLabels", tq.MissingLabels); err != nil {
   806  		return err
   807  	}
   808  
   809  	if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 {
   810  		return errors.New("both 'includedBranches' and 'excludedBranches' are specified ('excludedBranches' have no effect)")
   811  	}
   812  	if err := duplicates("includedBranches", tq.IncludedBranches); err != nil {
   813  		return err
   814  	}
   815  	if err := duplicates("excludedBranches", tq.ExcludedBranches); err != nil {
   816  		return err
   817  	}
   818  
   819  	return nil
   820  }
   821  
   822  // Validate returns an error if any contexts are listed more than once in the config.
   823  func (cp *TideContextPolicy) Validate() error {
   824  	if inter := sets.New[string](cp.RequiredContexts...).Intersection(sets.New[string](cp.OptionalContexts...)); inter.Len() > 0 {
   825  		return fmt.Errorf("contexts %s are defined as required and optional", strings.Join(sets.List(inter), ", "))
   826  	}
   827  	if inter := sets.New[string](cp.RequiredContexts...).Intersection(sets.New[string](cp.RequiredIfPresentContexts...)); inter.Len() > 0 {
   828  		return fmt.Errorf("contexts %s are defined as required and required if present", strings.Join(sets.List(inter), ", "))
   829  	}
   830  	if inter := sets.New[string](cp.OptionalContexts...).Intersection(sets.New[string](cp.RequiredIfPresentContexts...)); inter.Len() > 0 {
   831  		return fmt.Errorf("contexts %s are defined as optional and required if present", strings.Join(sets.List(inter), ", "))
   832  	}
   833  	return nil
   834  }
   835  
   836  func mergeTideContextPolicy(a, b TideContextPolicy) TideContextPolicy {
   837  	mergeBool := func(a, b *bool) *bool {
   838  		if b == nil {
   839  			return a
   840  		}
   841  		return b
   842  	}
   843  	c := TideContextPolicy{}
   844  	c.FromBranchProtection = mergeBool(a.FromBranchProtection, b.FromBranchProtection)
   845  	c.SkipUnknownContexts = mergeBool(a.SkipUnknownContexts, b.SkipUnknownContexts)
   846  	required := sets.New[string](a.RequiredContexts...)
   847  	requiredIfPresent := sets.New[string](a.RequiredIfPresentContexts...)
   848  	optional := sets.New[string](a.OptionalContexts...)
   849  	required.Insert(b.RequiredContexts...)
   850  	requiredIfPresent.Insert(b.RequiredIfPresentContexts...)
   851  	optional.Insert(b.OptionalContexts...)
   852  	if required.Len() > 0 {
   853  		c.RequiredContexts = sets.List(required)
   854  	}
   855  	if requiredIfPresent.Len() > 0 {
   856  		c.RequiredIfPresentContexts = sets.List(requiredIfPresent)
   857  	}
   858  	if optional.Len() > 0 {
   859  		c.OptionalContexts = sets.List(optional)
   860  	}
   861  	return c
   862  }
   863  
   864  func ParseTideContextPolicyOptions(org, repo, branch string, options TideContextPolicyOptions) TideContextPolicy {
   865  	option := options.TideContextPolicy
   866  	if o, ok := options.Orgs[org]; ok {
   867  		option = mergeTideContextPolicy(option, o.TideContextPolicy)
   868  		if r, ok := o.Repos[repo]; ok {
   869  			option = mergeTideContextPolicy(option, r.TideContextPolicy)
   870  			if b, ok := r.Branches[branch]; ok {
   871  				option = mergeTideContextPolicy(option, b)
   872  			}
   873  		}
   874  	}
   875  	return option
   876  }
   877  
   878  // GetTideContextPolicy parses the prow config to find context merge options.
   879  // If none are set, it will use the prow jobs configured and use the default github combined status.
   880  // Otherwise if set it will use the branch protection setting, or the listed jobs.
   881  func (c Config) GetTideContextPolicy(gitClient git.ClientFactory, org, repo, branch string, baseSHAGetter RefGetter, headSHA string) (*TideContextPolicy, error) {
   882  	var requireManuallyTriggeredJobs *bool
   883  	options := ParseTideContextPolicyOptions(org, repo, branch, c.Tide.ContextOptions)
   884  	// Adding required and optional contexts from options
   885  	required := sets.New[string](options.RequiredContexts...)
   886  	requiredIfPresent := sets.New[string](options.RequiredIfPresentContexts...)
   887  	optional := sets.New[string](options.OptionalContexts...)
   888  
   889  	headSHAGetter := func() (string, error) {
   890  		return headSHA, nil
   891  	}
   892  	presubmits, err := c.GetPresubmits(gitClient, org+"/"+repo, branch, baseSHAGetter, headSHAGetter)
   893  	if err != nil {
   894  		return nil, fmt.Errorf("failed to get presubmits: %w", err)
   895  	}
   896  
   897  	// Using Branch protection configuration
   898  	if options.FromBranchProtection != nil && *options.FromBranchProtection {
   899  		bp, err := c.GetBranchProtection(org, repo, branch, presubmits)
   900  		if err != nil {
   901  			logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch)
   902  		} else if bp != nil {
   903  			requireManuallyTriggeredJobs = bp.RequireManuallyTriggeredJobs
   904  			if bp.Protect != nil && *bp.Protect && bp.RequiredStatusChecks != nil {
   905  				required.Insert(bp.RequiredStatusChecks.Contexts...)
   906  			}
   907  		}
   908  	}
   909  
   910  	// automatically generate required and optional entries for Prow Jobs
   911  	prowRequired, prowRequiredIfPresent, prowOptional := BranchRequirements(branch, presubmits, requireManuallyTriggeredJobs)
   912  	required.Insert(prowRequired...)
   913  	requiredIfPresent.Insert(prowRequiredIfPresent...)
   914  	optional.Insert(prowOptional...)
   915  
   916  	t := &TideContextPolicy{
   917  		RequiredContexts:          sets.List(required),
   918  		RequiredIfPresentContexts: sets.List(requiredIfPresent),
   919  		OptionalContexts:          sets.List(optional),
   920  		SkipUnknownContexts:       options.SkipUnknownContexts,
   921  	}
   922  	if err := t.Validate(); err != nil {
   923  		return t, err
   924  	}
   925  	return t, nil
   926  }
   927  
   928  // IsOptional checks whether a context can be ignored.
   929  // Will return true if
   930  // - context is registered as optional
   931  // - required contexts are registered and the context provided is not required
   932  // Will return false otherwise. Every context is required.
   933  func (cp *TideContextPolicy) IsOptional(c string) bool {
   934  	if sets.New[string](cp.OptionalContexts...).Has(c) {
   935  		return true
   936  	}
   937  	if sets.New[string](cp.RequiredContexts...).Has(c) {
   938  		return false
   939  	}
   940  	// assume if we're asking that the context is present on the PR
   941  	if sets.New[string](cp.RequiredIfPresentContexts...).Has(c) {
   942  		return false
   943  	}
   944  	if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts {
   945  		return true
   946  	}
   947  	return false
   948  }
   949  
   950  // MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided.
   951  func (cp *TideContextPolicy) MissingRequiredContexts(contexts []string) []string {
   952  	if len(cp.RequiredContexts) == 0 {
   953  		return nil
   954  	}
   955  	existingContexts := sets.New[string]()
   956  	for _, c := range contexts {
   957  		existingContexts.Insert(c)
   958  	}
   959  	var missingContexts []string
   960  	for c := range sets.New[string](cp.RequiredContexts...).Difference(existingContexts) {
   961  		missingContexts = append(missingContexts, c)
   962  	}
   963  	return missingContexts
   964  }