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 }