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 }