sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/tide/github.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 tide
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    31  	"sigs.k8s.io/prow/pkg/config"
    32  	"sigs.k8s.io/prow/pkg/git/types"
    33  	"sigs.k8s.io/prow/pkg/git/v2"
    34  	"sigs.k8s.io/prow/pkg/github"
    35  	"sigs.k8s.io/prow/pkg/tide/blockers"
    36  
    37  	githubql "github.com/shurcooL/githubv4"
    38  	"github.com/sirupsen/logrus"
    39  )
    40  
    41  type querier func(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error
    42  
    43  func datedQuery(q string, start, end time.Time) string {
    44  	return fmt.Sprintf("%s %s", q, dateToken(start, end))
    45  }
    46  
    47  // Enforcing interface implementation check at compile time
    48  var _ provider = (*GitHubProvider)(nil)
    49  
    50  // GitHubProvider implements provider, used by tide Controller for
    51  // interacting directly with GitHub.
    52  //
    53  // Tide Controller should only use GitHubProvider for communicating with GitHub.
    54  type GitHubProvider struct {
    55  	cfg                config.Getter
    56  	ghc                githubClient
    57  	gc                 git.ClientFactory
    58  	usesGitHubAppsAuth bool
    59  
    60  	*mergeChecker
    61  	logger *logrus.Entry
    62  }
    63  
    64  func newGitHubProvider(
    65  	logger *logrus.Entry,
    66  	ghc githubClient,
    67  	gc git.ClientFactory,
    68  	cfg config.Getter,
    69  	mergeChecker *mergeChecker,
    70  	usesGitHubAppsAuth bool,
    71  ) *GitHubProvider {
    72  	return &GitHubProvider{
    73  		logger:             logger,
    74  		ghc:                ghc,
    75  		gc:                 gc,
    76  		cfg:                cfg,
    77  		usesGitHubAppsAuth: usesGitHubAppsAuth,
    78  		mergeChecker:       mergeChecker,
    79  	}
    80  }
    81  
    82  func (gi *GitHubProvider) blockers() (blockers.Blockers, error) {
    83  	label := gi.cfg().Tide.BlockerLabel
    84  	if label == "" {
    85  		return blockers.Blockers{}, nil
    86  	}
    87  
    88  	gi.logger.WithField("blocker_label", label).Debug("Searching for blocker issues")
    89  	orgExcepts, repos := gi.cfg().Tide.Queries.OrgExceptionsAndRepos()
    90  	orgs := make([]string, 0, len(orgExcepts))
    91  	for org := range orgExcepts {
    92  		orgs = append(orgs, org)
    93  	}
    94  	orgRepoQuery := orgRepoQueryStrings(orgs, repos.UnsortedList(), orgExcepts)
    95  	return blockers.FindAll(gi.ghc, gi.logger, label, orgRepoQuery, gi.usesGitHubAppsAuth)
    96  }
    97  
    98  // Query gets all open PRs based on tide configuration.
    99  func (gi *GitHubProvider) Query() (map[string]CodeReviewCommon, error) {
   100  	lock := sync.Mutex{}
   101  	wg := sync.WaitGroup{}
   102  	prs := make(map[string]CodeReviewCommon)
   103  	var errs []error
   104  	for i, query := range gi.cfg().Tide.Queries {
   105  
   106  		// Use org-sharded queries only when GitHub apps auth is in use
   107  		var queries map[string]string
   108  		if gi.usesGitHubAppsAuth {
   109  			queries = query.OrgQueries()
   110  		} else {
   111  			queries = map[string]string{"": query.Query()}
   112  		}
   113  
   114  		for org, q := range queries {
   115  			org, q, i := org, q, i
   116  			wg.Add(1)
   117  			go func() {
   118  				defer wg.Done()
   119  				results, err := gi.search(gi.ghc.QueryWithGitHubAppsSupport, gi.logger, q, time.Time{}, time.Now(), org)
   120  
   121  				resultString := "success"
   122  				if err != nil {
   123  					resultString = "error"
   124  				}
   125  				tideMetrics.queryResults.WithLabelValues(strconv.Itoa(i), org, resultString).Inc()
   126  
   127  				lock.Lock()
   128  				defer lock.Unlock()
   129  				if err != nil && len(results) == 0 {
   130  					gi.logger.WithField("query", q).WithError(err).Warn("Failed to execute query.")
   131  					errs = append(errs, fmt.Errorf("query %d, err: %w", i, err))
   132  					return
   133  				}
   134  				if err != nil {
   135  					gi.logger.WithError(err).WithField("query", q).Warning("found partial results")
   136  				}
   137  
   138  				for _, pr := range results {
   139  					crc := CodeReviewCommonFromPullRequest(&pr)
   140  					prs[prKey(crc)] = *crc
   141  				}
   142  			}()
   143  		}
   144  	}
   145  	wg.Wait()
   146  
   147  	return prs, utilerrors.NewAggregate(errs)
   148  }
   149  
   150  func (gi *GitHubProvider) GetRef(org, repo, ref string) (string, error) {
   151  	return gi.ghc.GetRef(org, repo, ref)
   152  }
   153  
   154  func (gi *GitHubProvider) GetTideContextPolicy(org, repo, branch string, baseSHAGetter config.RefGetter, pr *CodeReviewCommon) (contextChecker, error) {
   155  	return gi.cfg().GetTideContextPolicy(gi.gc, org, repo, branch, baseSHAGetter, pr.HeadRefOID)
   156  }
   157  
   158  func (gi *GitHubProvider) prMergeMethod(crc *CodeReviewCommon) *types.PullRequestMergeType {
   159  	return gi.mergeChecker.prMergeMethod(gi.cfg().Tide, crc)
   160  }
   161  
   162  func (gi *GitHubProvider) search(query querier, log *logrus.Entry, q string, start, end time.Time, org string) ([]PullRequest, error) {
   163  	start = floor(start)
   164  	end = floor(end)
   165  	log = log.WithFields(logrus.Fields{
   166  		"query": q,
   167  		"start": start.String(),
   168  		"end":   end.String(),
   169  	})
   170  	requestStart := time.Now()
   171  	var cursor *githubql.String
   172  	vars := map[string]interface{}{
   173  		"query":        githubql.String(datedQuery(q, start, end)),
   174  		"searchCursor": cursor,
   175  	}
   176  
   177  	var totalCost, remaining int
   178  	var ret []PullRequest
   179  	var sq searchQuery
   180  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
   181  	defer cancel()
   182  	for {
   183  		log.Debug("Sending query")
   184  		if err := query(ctx, &sq, vars, org); err != nil {
   185  			if cursor != nil {
   186  				err = fmt.Errorf("cursor: %q, err: %w", *cursor, err)
   187  			}
   188  			return ret, err
   189  		}
   190  		totalCost += int(sq.RateLimit.Cost)
   191  		remaining = int(sq.RateLimit.Remaining)
   192  		for _, n := range sq.Search.Nodes {
   193  			ret = append(ret, n.PullRequest)
   194  		}
   195  		if !sq.Search.PageInfo.HasNextPage {
   196  			break
   197  		}
   198  		cursor = &sq.Search.PageInfo.EndCursor
   199  		vars["searchCursor"] = cursor
   200  		log = log.WithField("searchCursor", *cursor)
   201  	}
   202  	log.WithFields(logrus.Fields{
   203  		"duration":       time.Since(requestStart).String(),
   204  		"pr_found_count": len(ret),
   205  		"cost":           totalCost,
   206  		"remaining":      remaining,
   207  	}).Debug("Finished query")
   208  	return ret, nil
   209  }
   210  
   211  func (gi *GitHubProvider) prepareMergeDetails(commitTemplates config.TideMergeCommitTemplate, pr CodeReviewCommon, mergeMethod types.PullRequestMergeType) github.MergeDetails {
   212  	ghMergeDetails := github.MergeDetails{
   213  		SHA:         pr.HeadRefOID,
   214  		MergeMethod: string(mergeMethod),
   215  	}
   216  
   217  	if commitTemplates.Title != nil {
   218  		var b bytes.Buffer
   219  
   220  		if err := commitTemplates.Title.Execute(&b, pr); err != nil {
   221  			gi.logger.Errorf("error executing commit title template: %v", err)
   222  		} else {
   223  			ghMergeDetails.CommitTitle = b.String()
   224  		}
   225  	}
   226  
   227  	if commitTemplates.Body != nil {
   228  		var b bytes.Buffer
   229  
   230  		if err := commitTemplates.Body.Execute(&b, pr); err != nil {
   231  			gi.logger.Errorf("error executing commit body template: %v", err)
   232  		} else {
   233  			ghMergeDetails.CommitMessage = b.String()
   234  		}
   235  	}
   236  
   237  	return ghMergeDetails
   238  }
   239  
   240  func (gi *GitHubProvider) mergePRs(sp subpool, prs []CodeReviewCommon, dontUpdateStatus *threadSafePRSet) ([]CodeReviewCommon, error) {
   241  	var merged []CodeReviewCommon
   242  	var failed []int
   243  	var errs []error
   244  	log := sp.log.WithField("merge-targets", prNumbers(prs))
   245  	tideConfig := gi.cfg().Tide
   246  
   247  	for i, pr := range prs {
   248  		log := log.WithFields(pr.logFields())
   249  		mergeMethod := gi.prMergeMethod(&pr)
   250  		if mergeMethod == nil {
   251  			err := fmt.Errorf("multiple merge method labels found for %s/%s#%d", sp.org, sp.repo, pr.Number)
   252  			log.WithError(err).Error("Multiple merge method labels are not supported.")
   253  			errs = append(errs, err)
   254  			failed = append(failed, pr.Number)
   255  			continue
   256  		}
   257  
   258  		// Ensure tide context has success state, otherwise PR merge will fail if branch protection
   259  		// in github is enabled and the loop to change tide context hasn't done it already
   260  		dontUpdateStatus.insert(sp.org, sp.repo, pr.Number)
   261  		if err := setTideStatusSuccess(pr, gi.ghc, gi.cfg(), log); err != nil {
   262  			log.WithError(err).Error("Unable to set tide context to SUCCESS.")
   263  			errs = append(errs, err)
   264  			failed = append(failed, pr.Number)
   265  			continue
   266  		}
   267  
   268  		commitTemplates := tideConfig.MergeCommitTemplate(config.OrgRepo{Org: sp.org, Repo: sp.repo})
   269  		keepTrying, err := tryMerge(func() error {
   270  			ghMergeDetails := gi.prepareMergeDetails(commitTemplates, pr, *mergeMethod)
   271  			return gi.ghc.Merge(sp.org, sp.repo, pr.Number, ghMergeDetails)
   272  		})
   273  		if err != nil {
   274  			// These are user errors, shouldn't be printed as tide errors
   275  			log.WithError(err).Debug("Merge failed.")
   276  		} else {
   277  			log.Info("Merged.")
   278  			merged = append(merged, pr)
   279  		}
   280  		if !keepTrying {
   281  			break
   282  		}
   283  		// If we successfully merged this PR and have more to merge, sleep to give
   284  		// GitHub time to recalculate mergeability.
   285  		if err == nil && i+1 < len(prs) {
   286  			sleep(time.Second * 5)
   287  		}
   288  	}
   289  
   290  	if len(errs) == 0 {
   291  		return merged, nil
   292  	}
   293  
   294  	// Construct a more informative error.
   295  	var batch string
   296  	if len(prs) > 1 {
   297  		batch = fmt.Sprintf(" from batch %v", prNumbers(prs))
   298  		if len(merged) > 0 {
   299  			batch = fmt.Sprintf("%s, partial merge %v", batch, prNumbers(merged))
   300  		}
   301  	}
   302  	return merged, fmt.Errorf("failed merging %v%s: %w", failed, batch, utilerrors.NewAggregate(errs))
   303  }
   304  
   305  // headContexts gets the status contexts for the commit with OID == pr.HeadRefOID
   306  //
   307  // First, we try to get this value from the commits we got with the PR query.
   308  // Unfortunately the 'last' commit ordering is determined by author date
   309  // not commit date so if commits are reordered non-chronologically on the PR
   310  // branch the 'last' commit isn't necessarily the logically last commit.
   311  // We list multiple commits with the query to increase our chance of success,
   312  // but if we don't find the head commit we have to ask GitHub for it
   313  // specifically (this costs an API token).
   314  //
   315  // This function is very GitHub centric, make sure this that is only referenced
   316  // by GitHub interactor.
   317  func (gi *GitHubProvider) headContexts(pr *CodeReviewCommon) ([]Context, error) {
   318  	log := gi.logger
   319  	commits := pr.GitHubCommits()
   320  	if commits != nil {
   321  		for _, node := range commits.Nodes {
   322  			if string(node.Commit.OID) == pr.HeadRefOID {
   323  				return append(node.Commit.Status.Contexts, checkRunNodesToContexts(log, node.Commit.StatusCheckRollup.Contexts.Nodes)...), nil
   324  			}
   325  		}
   326  	}
   327  	// We didn't get the head commit from the query (the commits must not be
   328  	// logically ordered) so we need to specifically ask GitHub for the status
   329  	// and coerce it to a graphql type.
   330  	org := pr.Org
   331  	repo := pr.Repo
   332  	// Log this event so we can tune the number of commits we list to minimize this.
   333  	// TODO alvaroaleman: Add checkrun support here. Doesn't seem to happen often though,
   334  	// openshift doesn't have a single occurrence of this in the past seven days.
   335  	log.Warnf("'last' %d commits didn't contain logical last commit. Querying GitHub...", len(commits.Nodes))
   336  	combined, err := gi.ghc.GetCombinedStatus(org, repo, pr.HeadRefOID)
   337  	if err != nil {
   338  		return nil, fmt.Errorf("failed to get the combined status: %w", err)
   339  	}
   340  	checkRunList, err := gi.ghc.ListCheckRuns(org, repo, pr.HeadRefOID)
   341  	if err != nil {
   342  		return nil, fmt.Errorf("Failed to list checkruns: %w", err)
   343  	}
   344  	checkRunNodes := make([]CheckRunNode, 0, len(checkRunList.CheckRuns))
   345  	for _, checkRun := range checkRunList.CheckRuns {
   346  		checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: CheckRun{
   347  			Name: githubql.String(checkRun.Name),
   348  			// They are uppercase in the V4 api and lowercase in the V3 api
   349  			Conclusion: githubql.String(strings.ToUpper(checkRun.Conclusion)),
   350  			Status:     githubql.String(strings.ToUpper(checkRun.Status)),
   351  		}})
   352  	}
   353  
   354  	contexts := make([]Context, 0, len(combined.Statuses)+len(checkRunNodes))
   355  	for _, status := range combined.Statuses {
   356  		contexts = append(contexts, Context{
   357  			Context:     githubql.String(status.Context),
   358  			Description: githubql.String(status.Description),
   359  			State:       githubql.StatusState(strings.ToUpper(status.State)),
   360  		})
   361  	}
   362  	contexts = append(contexts, checkRunNodesToContexts(log, checkRunNodes)...)
   363  
   364  	// Add a commit with these contexts to pr for future look ups.
   365  	if commits := pr.GitHubCommits(); commits != nil {
   366  		commits.Nodes = append(commits.Nodes,
   367  			struct{ Commit Commit }{
   368  				Commit: Commit{
   369  					OID:    githubql.String(pr.HeadRefOID),
   370  					Status: struct{ Contexts []Context }{Contexts: contexts},
   371  				},
   372  			},
   373  		)
   374  	}
   375  	return contexts, nil
   376  }
   377  
   378  func (gi *GitHubProvider) GetPresubmits(identifier, baseBranch string, baseSHAGetter config.RefGetter, headSHAGetters ...config.RefGetter) ([]config.Presubmit, error) {
   379  	return gi.cfg().GetPresubmits(gi.gc, identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   380  }
   381  
   382  func (gi *GitHubProvider) GetChangedFiles(org, repo string, number int) ([]string, error) {
   383  	changes, err := gi.ghc.GetPullRequestChanges(org, repo, number)
   384  	if err != nil {
   385  		return nil, fmt.Errorf("failed get PR changes: %v", err)
   386  	}
   387  	files := make([]string, 0, len(changes))
   388  	for _, c := range changes {
   389  		files = append(files, c.Filename)
   390  	}
   391  	return files, nil
   392  }
   393  
   394  func (gi *GitHubProvider) refsForJob(sp subpool, prs []CodeReviewCommon) (prowapi.Refs, error) {
   395  	refs := prowapi.Refs{
   396  		Org:     sp.org,
   397  		Repo:    sp.repo,
   398  		BaseRef: sp.branch,
   399  		BaseSHA: sp.sha,
   400  	}
   401  	for _, pr := range prs {
   402  		refs.Pulls = append(
   403  			refs.Pulls,
   404  			prowapi.Pull{
   405  				Number:  pr.Number,
   406  				Title:   pr.Title,
   407  				Author:  string(pr.AuthorLogin),
   408  				SHA:     pr.HeadRefOID,
   409  				HeadRef: pr.HeadRefName,
   410  			},
   411  		)
   412  	}
   413  	return refs, nil
   414  }
   415  
   416  func (gi *GitHubProvider) labelsAndAnnotations(instance string, jobLabels, jobAnnotations map[string]string, changes ...CodeReviewCommon) (labels, annotations map[string]string) {
   417  	labels, annotations = jobLabels, jobAnnotations
   418  	return
   419  }
   420  
   421  func (gi *GitHubProvider) jobIsRequiredByTide(ps *config.Presubmit, pr *CodeReviewCommon) bool {
   422  	return ps.ContextRequired() || ps.RunBeforeMerge
   423  }
   424  
   425  // dateToken generates a GitHub search query token for the specified date range.
   426  // See: https://help.github.com/articles/understanding-the-search-syntax/#query-for-dates
   427  func dateToken(start, end time.Time) string {
   428  	// GitHub's GraphQL API silently fails if you provide it with an invalid time
   429  	// string.
   430  	// Dates before 1970 (unix epoch) are considered invalid.
   431  	startString, endString := "*", "*"
   432  	if start.Year() >= 1970 {
   433  		startString = start.Format(github.SearchTimeFormat)
   434  	}
   435  	if end.Year() >= 1970 {
   436  		endString = end.Format(github.SearchTimeFormat)
   437  	}
   438  	return fmt.Sprintf("updated:%s..%s", startString, endString)
   439  }
   440  
   441  func floor(t time.Time) time.Time {
   442  	if t.Before(github.FoundingYear) {
   443  		return github.FoundingYear
   444  	}
   445  	return t
   446  }
   447  
   448  // mergeChecker provides a function to check if a PR can be merged with
   449  // the requested method and does not have a merge conflict.
   450  // It caches results and should be cleared periodically with clearCache()
   451  //
   452  // This struct is GitHub specific, and should be used only by GitHubProvider.
   453  type mergeChecker struct {
   454  	config config.Getter
   455  	ghc    githubClient
   456  
   457  	sync.Mutex
   458  	cache map[config.OrgRepo]map[types.PullRequestMergeType]bool
   459  }
   460  
   461  // newMergeChecker creates a mergeChecker for GitHub, and should be used only by
   462  // GitHubProvider.
   463  func newMergeChecker(cfg config.Getter, ghc githubClient) *mergeChecker {
   464  	m := &mergeChecker{
   465  		config: cfg,
   466  		ghc:    ghc,
   467  		cache:  map[config.OrgRepo]map[types.PullRequestMergeType]bool{},
   468  	}
   469  
   470  	go m.clearCache()
   471  	return m
   472  }
   473  
   474  // clearCache is an internal function that's only used by newMergeChecker.
   475  func (m *mergeChecker) clearCache() {
   476  	// Only do this once per token reset since it could be a bit expensive for
   477  	// Tide instances that handle hundreds of repos.
   478  	ticker := time.NewTicker(time.Hour)
   479  	for {
   480  		<-ticker.C
   481  		m.Lock()
   482  		m.cache = make(map[config.OrgRepo]map[types.PullRequestMergeType]bool)
   483  		m.Unlock()
   484  	}
   485  }
   486  
   487  // repoMethods is used only by isAllowedToMerge.
   488  //
   489  // As a result it is also referenced only from GitHubProvider.
   490  func (m *mergeChecker) repoMethods(orgRepo config.OrgRepo) (map[types.PullRequestMergeType]bool, error) {
   491  	m.Lock()
   492  	defer m.Unlock()
   493  
   494  	repoMethods, ok := m.cache[orgRepo]
   495  	if !ok {
   496  		fullRepo, err := m.ghc.GetRepo(orgRepo.Org, orgRepo.Repo)
   497  		if err != nil {
   498  			return nil, err
   499  		}
   500  		logrus.WithFields(logrus.Fields{
   501  			"org":              orgRepo.Org,
   502  			"repo":             orgRepo.Repo,
   503  			"AllowMergeCommit": fullRepo.AllowMergeCommit,
   504  			"AllowSquashMerge": fullRepo.AllowSquashMerge,
   505  			"AllowRebaseMerge": fullRepo.AllowRebaseMerge,
   506  		}).Debug("GetRepo returns these values for repo methods")
   507  		repoMethods = map[types.PullRequestMergeType]bool{
   508  			types.MergeMerge:  fullRepo.AllowMergeCommit,
   509  			types.MergeSquash: fullRepo.AllowSquashMerge,
   510  			types.MergeRebase: fullRepo.AllowRebaseMerge,
   511  		}
   512  		m.cache[orgRepo] = repoMethods
   513  	}
   514  	return repoMethods, nil
   515  }
   516  
   517  // isAllowed checks if a PR does not have merge conflicts and requests an
   518  // allowed merge method. If there is no error it returns a string explanation if
   519  // not allowed or "" if allowed.
   520  func (m *mergeChecker) isAllowedToMerge(crc *CodeReviewCommon) (string, error) {
   521  	// Get PullRequest struct for GitHub specific logic
   522  	pr := crc.GitHub
   523  	if pr == nil {
   524  		// This should not happen, as this mergeChecker is meant to be used by
   525  		// GitHub repos only
   526  		return "", errors.New("unexpected error: CodeReviewCommon should carry PullRequest struct")
   527  	}
   528  	if pr.Mergeable == githubql.MergeableStateConflicting {
   529  		return "PR has a merge conflict.", nil
   530  	}
   531  	mergeMethod := m.prMergeMethod(m.config().Tide, crc)
   532  	if mergeMethod == nil {
   533  		// Can happen when tide has conflicting labels: merge, squash, rebase
   534  		return "PR has conflicting merge method override labels", nil
   535  	}
   536  	if *mergeMethod == types.MergeRebase && !pr.CanBeRebased {
   537  		return "PR can't be rebased", nil
   538  	}
   539  	orgRepo := config.OrgRepo{Org: crc.Org, Repo: crc.Repo}
   540  	repoMethods, err := m.repoMethods(orgRepo)
   541  	if err != nil {
   542  		return "", fmt.Errorf("error getting repo data: %w", err)
   543  	}
   544  	if allowed, exists := repoMethods[*mergeMethod]; !exists {
   545  		// Should be impossible as well.
   546  		return "", fmt.Errorf("Programmer error! PR requested the unrecognized merge type %q", *mergeMethod)
   547  	} else if !allowed {
   548  		return fmt.Sprintf("Merge type %q disallowed by repo settings", *mergeMethod), nil
   549  	}
   550  	return "", nil
   551  }
   552  
   553  // prMergeMethod figures out merge method based on tide config, this could be
   554  // overridden by GitHub labels.
   555  func (mc *mergeChecker) prMergeMethod(c config.Tide, crc *CodeReviewCommon) *types.PullRequestMergeType {
   556  	repo := config.OrgRepo{Org: crc.Org, Repo: crc.Repo}
   557  	method := c.OrgRepoBranchMergeMethod(repo, crc.BaseRefName)
   558  	squashLabel := c.SquashLabel
   559  	rebaseLabel := c.RebaseLabel
   560  	mergeLabel := c.MergeLabel
   561  	if squashLabel != "" || rebaseLabel != "" || mergeLabel != "" {
   562  		labelCount := 0
   563  		if labels := crc.GitHubLabels(); labels != nil {
   564  			for _, prlabel := range labels.Nodes {
   565  				switch string(prlabel.Name) {
   566  				case "":
   567  					continue
   568  				case squashLabel:
   569  					method = types.MergeSquash
   570  					labelCount++
   571  				case rebaseLabel:
   572  					method = types.MergeRebase
   573  					labelCount++
   574  				case mergeLabel:
   575  					method = types.MergeMerge
   576  					labelCount++
   577  				}
   578  				if labelCount > 1 {
   579  					return nil
   580  				}
   581  			}
   582  		}
   583  	}
   584  	return &method
   585  }