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