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  }