github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/blunderbuss.go (about) 1 /* 2 Copyright 2015 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 mungers 18 19 import ( 20 "math" 21 "math/rand" 22 23 "k8s.io/apimachinery/pkg/util/sets" 24 "k8s.io/test-infra/mungegithub/features" 25 "k8s.io/test-infra/mungegithub/github" 26 "k8s.io/test-infra/mungegithub/options" 27 28 "github.com/golang/glog" 29 githubapi "github.com/google/go-github/github" 30 ) 31 32 // weightMap is a map of user to a weight for that user. 33 type weightMap map[string]int64 34 35 // A BlunderbussConfig maps a set of file prefixes to a set of owner names (github users) 36 type BlunderbussConfig struct { 37 PrefixMap map[string][]string `json:"prefixMap,omitempty" yaml:"prefixMap,omitempty"` 38 } 39 40 // BlunderbussMunger will assign issues to users based on the config file 41 // provided by --blunderbuss-config and/or OWNERS files. 42 type BlunderbussMunger struct { 43 config *BlunderbussConfig 44 features *features.Features 45 46 blunderbussReassign bool 47 numAssignees int 48 } 49 50 func init() { 51 blunderbuss := &BlunderbussMunger{} 52 RegisterMungerOrDie(blunderbuss) 53 } 54 55 // Name is the name usable in --pr-mungers 56 func (b *BlunderbussMunger) Name() string { return "blunderbuss" } 57 58 // RequiredFeatures is a slice of 'features' that must be provided 59 func (b *BlunderbussMunger) RequiredFeatures() []string { 60 return []string{features.RepoFeatureName} 61 } 62 63 // Initialize will initialize the munger 64 func (b *BlunderbussMunger) Initialize(config *github.Config, features *features.Features) error { 65 b.features = features 66 return nil 67 } 68 69 // EachLoop is called at the start of every munge loop 70 func (b *BlunderbussMunger) EachLoop() error { return nil } 71 72 // RegisterOptions registers options for this munger; returns any that require a restart when changed. 73 func (b *BlunderbussMunger) RegisterOptions(opts *options.Options) sets.String { 74 opts.RegisterBool(&b.blunderbussReassign, "blunderbuss-reassign", false, "Assign PRs even if they're already assigned; use with -dry-run to judge changes to the assignment algorithm") 75 opts.RegisterInt(&b.numAssignees, "blunderbuss-number-assignees", 2, "Number of assignees to select for each PR.") 76 return nil 77 } 78 79 func chance(val, total int64) float64 { 80 return 100.0 * float64(val) / float64(total) 81 } 82 83 func printChance(owners weightMap, total int64) { 84 if !glog.V(4) { 85 return 86 } 87 glog.Infof("Owner\tPercent") 88 for name, weight := range owners { 89 glog.Infof("%s\t%02.2f%%", name, chance(weight, total)) 90 } 91 } 92 93 func getPotentialOwners(author string, feats *features.Features, files []*githubapi.CommitFile, leafOnly bool) (weightMap, int64) { 94 potentialOwners := weightMap{} 95 weightSum := int64(0) 96 var fileOwners sets.String 97 for _, file := range files { 98 if file == nil { 99 continue 100 } 101 fileWeight := int64(1) 102 if file.Changes != nil && *file.Changes != 0 { 103 fileWeight = int64(*file.Changes) 104 } 105 // Judge file size on a log scale-- effectively this 106 // makes three buckets, we shouldn't have many 10k+ 107 // line changes. 108 fileWeight = int64(math.Log10(float64(fileWeight))) + 1 109 if leafOnly { 110 fileOwners = feats.Repos.LeafReviewers(*file.Filename) 111 } else { 112 fileOwners = feats.Repos.Reviewers(*file.Filename) 113 } 114 115 if fileOwners.Len() == 0 { 116 glog.Warningf("Couldn't find an owner for: %s", *file.Filename) 117 } 118 119 for _, owner := range fileOwners.List() { 120 if owner == author { 121 continue 122 } 123 potentialOwners[owner] = potentialOwners[owner] + fileWeight 124 weightSum += fileWeight 125 } 126 } 127 return potentialOwners, weightSum 128 } 129 130 func selectMultipleOwners(potentialOwners weightMap, weightSum int64, count int) []string { 131 // Make a copy of the map 132 pOwners := weightMap{} 133 for k, v := range potentialOwners { 134 pOwners[k] = v 135 } 136 137 owners := []string{} 138 139 for i := 0; i < count; i++ { 140 if len(pOwners) == 0 || weightSum == 0 { 141 break 142 } 143 selection := rand.Int63n(weightSum) 144 owner := "" 145 for o, w := range pOwners { 146 owner = o 147 selection -= w 148 if selection <= 0 { 149 break 150 } 151 } 152 153 owners = append(owners, owner) 154 weightSum -= pOwners[owner] 155 156 // Remove this person from the map. 157 delete(pOwners, owner) 158 } 159 return owners 160 } 161 162 // Munge is the workhorse the will actually make updates to the PR 163 func (b *BlunderbussMunger) Munge(obj *github.MungeObject) { 164 if !obj.IsPR() { 165 return 166 } 167 168 issue := obj.Issue 169 if !b.blunderbussReassign && issue.Assignee != nil { 170 glog.V(6).Infof("skipping %v: reassign: %v assignee: %v", *issue.Number, b.blunderbussReassign, github.DescribeUser(issue.Assignee)) 171 return 172 } 173 174 files, ok := obj.ListFiles() 175 if !ok { 176 return 177 } 178 179 potentialOwners, weightSum := getPotentialOwners(*obj.Issue.User.Login, b.features, files, true) 180 if len(potentialOwners) == 0 { 181 potentialOwners, weightSum = getPotentialOwners(*obj.Issue.User.Login, b.features, files, false) 182 if len(potentialOwners) == 0 { 183 glog.Errorf("No OWNERS found for PR %d", *issue.Number) 184 return 185 } 186 } 187 printChance(potentialOwners, weightSum) 188 if issue.Assignee != nil { 189 cur := *issue.Assignee.Login 190 c := chance(potentialOwners[cur], weightSum) 191 glog.Infof("Current assignee %v has a %02.2f%% chance of having been chosen", cur, c) 192 } 193 194 owners := selectMultipleOwners(potentialOwners, weightSum, b.numAssignees) 195 196 for _, owner := range owners { 197 c := chance(potentialOwners[owner], weightSum) 198 glog.Infof("Assigning %v to %v who had a %02.2f%% chance to be assigned", *issue.Number, owner, c) 199 obj.AddAssignee(owner) 200 } 201 }