github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/size/size.go (about)

     1  /*
     2  Copyright 2016 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 size contains a Prow plugin which counts the number of lines changed
    18  // in a pull request, buckets this number into a few size classes (S, L, XL, etc),
    19  // and finally labels the pull request with this size.
    20  package size
    21  
    22  import (
    23  	"fmt"
    24  	"strings"
    25  
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"sigs.k8s.io/prow/pkg/config"
    29  	"sigs.k8s.io/prow/pkg/genfiles"
    30  	"sigs.k8s.io/prow/pkg/gitattributes"
    31  	"sigs.k8s.io/prow/pkg/github"
    32  	"sigs.k8s.io/prow/pkg/pluginhelp"
    33  	"sigs.k8s.io/prow/pkg/plugins"
    34  )
    35  
    36  // The sizes are configurable in the `plugins.yaml` config file; the line constants
    37  // in here represent default values used as fallback if none are provided.
    38  const pluginName = "size"
    39  
    40  var defaultSizes = plugins.Size{
    41  	S:   10,
    42  	M:   30,
    43  	L:   100,
    44  	Xl:  500,
    45  	Xxl: 1000,
    46  }
    47  
    48  func init() {
    49  	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
    50  }
    51  
    52  func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
    53  	sizes := sizesOrDefault(config.Size)
    54  	yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{
    55  		Size: plugins.Size{
    56  			S:   10,
    57  			M:   30,
    58  			L:   100,
    59  			Xl:  500,
    60  			Xxl: 1000,
    61  		},
    62  	})
    63  	if err != nil {
    64  		logrus.WithError(err).Warnf("cannot generate comments for %s plugin", pluginName)
    65  	}
    66  	return &pluginhelp.PluginHelp{
    67  			Description: "The size plugin manages the 'size/*' labels, maintaining the appropriate label on each pull request as it is updated. Generated files identified by the config file '.generated_files' at the repo root are ignored. Labels are applied based on the total number of lines of changes (additions and deletions).",
    68  			Config: map[string]string{
    69  				"": fmt.Sprintf(`The plugin has the following thresholds:<ul>
    70  <li>size/XS:  0-%d</li>
    71  <li>size/S:   %d-%d</li>
    72  <li>size/M:   %d-%d</li>
    73  <li>size/L:   %d-%d</li>
    74  <li>size/XL:  %d-%d</li>
    75  <li>size/XXL: %d+</li>
    76  </ul>`, sizes.S-1, sizes.S, sizes.M-1, sizes.M, sizes.L-1, sizes.L, sizes.Xl-1, sizes.Xl, sizes.Xxl-1, sizes.Xxl),
    77  			},
    78  			Snippet: yamlSnippet,
    79  		},
    80  		nil
    81  }
    82  
    83  func handlePullRequest(pc plugins.Agent, pe github.PullRequestEvent) error {
    84  	return handlePR(pc.GitHubClient, sizesOrDefault(pc.PluginConfig.Size), pc.Logger, pe)
    85  }
    86  
    87  // Strict subset of github.Client methods.
    88  type githubClient interface {
    89  	AddLabel(owner, repo string, number int, label string) error
    90  	RemoveLabel(owner, repo string, number int, label string) error
    91  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    92  	GetFile(org, repo, filepath, commit string) ([]byte, error)
    93  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
    94  }
    95  
    96  func handlePR(gc githubClient, sizes plugins.Size, le *logrus.Entry, pe github.PullRequestEvent) error {
    97  	if !isPRChanged(pe) {
    98  		return nil
    99  	}
   100  
   101  	var (
   102  		owner = pe.PullRequest.Base.Repo.Owner.Login
   103  		repo  = pe.PullRequest.Base.Repo.Name
   104  		num   = pe.PullRequest.Number
   105  		sha   = pe.PullRequest.Base.SHA
   106  	)
   107  
   108  	gf, err := genfiles.NewGroup(gc, owner, repo, sha)
   109  	if err != nil {
   110  		switch err.(type) {
   111  		case *genfiles.ParseError:
   112  			// Continue on parse errors, but warn that something is wrong.
   113  			le.Warnf("error while parsing .generated_files: %v", err)
   114  		default:
   115  			return err
   116  		}
   117  	}
   118  
   119  	ga, err := gitattributes.NewGroup(func() ([]byte, error) { return gc.GetFile(owner, repo, ".gitattributes", sha) })
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	changes, err := gc.GetPullRequestChanges(owner, repo, num)
   125  	if err != nil {
   126  		return fmt.Errorf("can not get PR changes for size plugin: %w", err)
   127  	}
   128  
   129  	var count int
   130  	for _, change := range changes {
   131  		// Skip generated and linguist-generated files.
   132  		if gf.Match(change.Filename) || ga.IsLinguistGenerated(change.Filename) {
   133  			continue
   134  		}
   135  
   136  		count += change.Additions + change.Deletions
   137  	}
   138  
   139  	labels, err := gc.GetIssueLabels(owner, repo, num)
   140  	if err != nil {
   141  		le.Warnf("while retrieving labels, error: %v", err)
   142  	}
   143  
   144  	newLabel := bucket(count, sizes).label()
   145  	var hasLabel bool
   146  
   147  	for _, label := range labels {
   148  		if label.Name == newLabel {
   149  			hasLabel = true
   150  			continue
   151  		}
   152  
   153  		if strings.HasPrefix(label.Name, labelPrefix) {
   154  			if err := gc.RemoveLabel(owner, repo, num, label.Name); err != nil {
   155  				le.Warnf("error while removing label %q: %v", label.Name, err)
   156  			}
   157  		}
   158  	}
   159  
   160  	if hasLabel {
   161  		return nil
   162  	}
   163  
   164  	if err := gc.AddLabel(owner, repo, num, newLabel); err != nil {
   165  		return fmt.Errorf("error adding label to %s/%s PR #%d: %w", owner, repo, num, err)
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  // One of a set of discrete buckets.
   172  type size int
   173  
   174  const (
   175  	sizeXS size = iota
   176  	sizeS
   177  	sizeM
   178  	sizeL
   179  	sizeXL
   180  	sizeXXL
   181  )
   182  
   183  const (
   184  	labelPrefix = "size/"
   185  
   186  	labelXS      = "size/XS"
   187  	labelS       = "size/S"
   188  	labelM       = "size/M"
   189  	labelL       = "size/L"
   190  	labelXL      = "size/XL"
   191  	labelXXL     = "size/XXL"
   192  	labelUnknown = "size/?"
   193  )
   194  
   195  func (s size) label() string {
   196  	switch s {
   197  	case sizeXS:
   198  		return labelXS
   199  	case sizeS:
   200  		return labelS
   201  	case sizeM:
   202  		return labelM
   203  	case sizeL:
   204  		return labelL
   205  	case sizeXL:
   206  		return labelXL
   207  	case sizeXXL:
   208  		return labelXXL
   209  	}
   210  
   211  	return labelUnknown
   212  }
   213  
   214  func bucket(lineCount int, sizes plugins.Size) size {
   215  	if lineCount < sizes.S {
   216  		return sizeXS
   217  	} else if lineCount < sizes.M {
   218  		return sizeS
   219  	} else if lineCount < sizes.L {
   220  		return sizeM
   221  	} else if lineCount < sizes.Xl {
   222  		return sizeL
   223  	} else if lineCount < sizes.Xxl {
   224  		return sizeXL
   225  	}
   226  
   227  	return sizeXXL
   228  }
   229  
   230  // These are the only actions indicating the code diffs may have changed.
   231  func isPRChanged(pe github.PullRequestEvent) bool {
   232  	switch pe.Action {
   233  	case github.PullRequestActionOpened:
   234  		return true
   235  	case github.PullRequestActionReopened:
   236  		return true
   237  	case github.PullRequestActionSynchronize:
   238  		return true
   239  	case github.PullRequestActionEdited:
   240  		return true
   241  	default:
   242  		return false
   243  	}
   244  }
   245  
   246  func defaultIfZero(value, defaultValue int) int {
   247  	if value == 0 {
   248  		return defaultValue
   249  	}
   250  	return value
   251  }
   252  
   253  func sizesOrDefault(sizes plugins.Size) plugins.Size {
   254  	sizes.S = defaultIfZero(sizes.S, defaultSizes.S)
   255  	sizes.M = defaultIfZero(sizes.M, defaultSizes.M)
   256  	sizes.L = defaultIfZero(sizes.L, defaultSizes.L)
   257  	sizes.Xl = defaultIfZero(sizes.Xl, defaultSizes.Xl)
   258  	sizes.Xxl = defaultIfZero(sizes.Xxl, defaultSizes.Xxl)
   259  	return sizes
   260  }