github.com/abayer/test-infra@v0.0.5/prow/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  	"k8s.io/test-infra/prow/genfiles"
    29  	"k8s.io/test-infra/prow/github"
    30  	"k8s.io/test-infra/prow/pluginhelp"
    31  	"k8s.io/test-infra/prow/plugins"
    32  )
    33  
    34  const pluginName = "size"
    35  
    36  func init() {
    37  	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
    38  }
    39  
    40  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    41  	// Only the Description field is specified because this plugin is not triggered with commands and is not configurable.
    42  	return &pluginhelp.PluginHelp{
    43  			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):<ul>
    44  <li>size/XS:	0-9</li>
    45  <li>size/S:	10-29</li>
    46  <li>size/M:	30-99</li>
    47  <li>size/L	100-499</li>
    48  <li>size/XL:	500-999</li>
    49  <li>size/XXL:	1000+</li>
    50  </ul>`,
    51  		},
    52  		nil
    53  }
    54  
    55  func handlePullRequest(pc plugins.PluginClient, pe github.PullRequestEvent) error {
    56  	return handlePR(pc.GitHubClient, pc.Logger, pe)
    57  }
    58  
    59  // Strict subset of *github.Client methods.
    60  type githubClient interface {
    61  	AddLabel(owner, repo string, number int, label string) error
    62  	RemoveLabel(owner, repo string, number int, label string) error
    63  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    64  	GetFile(org, repo, filepath, commit string) ([]byte, error)
    65  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
    66  }
    67  
    68  func handlePR(gc githubClient, le *logrus.Entry, pe github.PullRequestEvent) error {
    69  	if !isPRChanged(pe) {
    70  		return nil
    71  	}
    72  
    73  	var (
    74  		owner = pe.PullRequest.Base.Repo.Owner.Login
    75  		repo  = pe.PullRequest.Base.Repo.Name
    76  		num   = pe.PullRequest.Number
    77  		sha   = pe.PullRequest.Base.SHA
    78  	)
    79  
    80  	g, err := genfiles.NewGroup(gc, owner, repo, sha)
    81  	if err != nil {
    82  		switch err.(type) {
    83  		case *genfiles.ParseError:
    84  			// Continue on parse errors, but warn that something is wrong.
    85  			le.Warnf("error while parsing .generated_files: %v", err)
    86  		default:
    87  			return err
    88  		}
    89  	}
    90  
    91  	changes, err := gc.GetPullRequestChanges(owner, repo, num)
    92  	if err != nil {
    93  		return fmt.Errorf("can not get PR changes for size plugin: %v", err)
    94  	}
    95  
    96  	var count int
    97  	for _, change := range changes {
    98  		if g.Match(change.Filename) {
    99  			continue
   100  		}
   101  
   102  		count += change.Additions + change.Deletions
   103  	}
   104  
   105  	labels, err := gc.GetIssueLabels(owner, repo, num)
   106  	if err != nil {
   107  		le.Warnf("while retrieving labels, error: %v", err)
   108  	}
   109  
   110  	newLabel := bucket(count).label()
   111  	var hasLabel bool
   112  
   113  	for _, label := range labels {
   114  		if label.Name == newLabel {
   115  			hasLabel = true
   116  			continue
   117  		}
   118  
   119  		if strings.HasPrefix(label.Name, labelPrefix) {
   120  			if err := gc.RemoveLabel(owner, repo, num, label.Name); err != nil {
   121  				le.Warnf("error while removing label %q: %v", label.Name, err)
   122  			}
   123  		}
   124  	}
   125  
   126  	if hasLabel {
   127  		return nil
   128  	}
   129  
   130  	if err := gc.AddLabel(owner, repo, num, newLabel); err != nil {
   131  		return fmt.Errorf("error adding label to %s/%s PR #%d: %v", owner, repo, num, err)
   132  	}
   133  
   134  	return nil
   135  }
   136  
   137  // One of a set of discrete buckets.
   138  type size int
   139  
   140  const (
   141  	sizeXS size = iota
   142  	sizeS
   143  	sizeM
   144  	sizeL
   145  	sizeXL
   146  	sizeXXL
   147  )
   148  
   149  const (
   150  	labelPrefix = "size/"
   151  
   152  	labelXS     = "size/XS"
   153  	labelS      = "size/S"
   154  	labelM      = "size/M"
   155  	labelL      = "size/L"
   156  	labelXL     = "size/XL"
   157  	labelXXL    = "size/XXL"
   158  	labelUnkown = "size/?"
   159  )
   160  
   161  func (s size) label() string {
   162  	switch s {
   163  	case sizeXS:
   164  		return labelXS
   165  	case sizeS:
   166  		return labelS
   167  	case sizeM:
   168  		return labelM
   169  	case sizeL:
   170  		return labelL
   171  	case sizeXL:
   172  		return labelXL
   173  	case sizeXXL:
   174  		return labelXXL
   175  	}
   176  
   177  	return labelUnkown
   178  }
   179  
   180  func bucket(lineCount int) size {
   181  	if lineCount < 10 {
   182  		return sizeXS
   183  	} else if lineCount < 30 {
   184  		return sizeS
   185  	} else if lineCount < 100 {
   186  		return sizeM
   187  	} else if lineCount < 500 {
   188  		return sizeL
   189  	} else if lineCount < 1000 {
   190  		return sizeXL
   191  	}
   192  
   193  	return sizeXXL
   194  }
   195  
   196  // These are the only actions indicating the code diffs may have changed.
   197  func isPRChanged(pe github.PullRequestEvent) bool {
   198  	switch pe.Action {
   199  	case github.PullRequestActionOpened:
   200  		return true
   201  	case github.PullRequestActionReopened:
   202  		return true
   203  	case github.PullRequestActionSynchronize:
   204  		return true
   205  	case github.PullRequestActionEdited:
   206  		return true
   207  	default:
   208  		return false
   209  	}
   210  }