github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/label/label.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 label
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  
    24  	"github.com/sirupsen/logrus"
    25  
    26  	"k8s.io/test-infra/prow/github"
    27  	"k8s.io/test-infra/prow/pluginhelp"
    28  	"k8s.io/test-infra/prow/plugins"
    29  )
    30  
    31  const pluginName = "label"
    32  
    33  var (
    34  	labelRegex              = regexp.MustCompile(`(?m)^/(area|committee|kind|language|priority|sig|triage|wg)\s*(.*)$`)
    35  	removeLabelRegex        = regexp.MustCompile(`(?m)^/remove-(area|committee|kind|language|priority|sig|triage|wg)\s*(.*)$`)
    36  	customLabelRegex        = regexp.MustCompile(`(?m)^/label\s*(.*)$`)
    37  	customRemoveLabelRegex  = regexp.MustCompile(`(?m)^/remove-label\s*(.*)$`)
    38  	nonExistentLabelOnIssue = "Those labels are not set on the issue: `%v`"
    39  )
    40  
    41  func init() {
    42  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    43  }
    44  
    45  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    46  	// The Config field is omitted because this plugin is not configurable.
    47  	pluginHelp := &pluginhelp.PluginHelp{
    48  		Description: "The label plugin provides commands that add or remove certain types of labels. Labels of the following types can be manipulated: 'area/*', 'committee/*', 'kind/*', 'language/*', 'priority/*', 'sig/*', 'triage/*', and 'wg/*'. More labels can be configured to be used via the /label command.",
    49  	}
    50  	pluginHelp.AddCommand(pluginhelp.Command{
    51  		Usage:       "/[remove-](area|committee|kind|language|priority|sig|triage|wg|label) <target>",
    52  		Description: "Applies or removes a label from one of the recognized types of labels.",
    53  		Featured:    false,
    54  		WhoCanUse:   "Anyone can trigger this command on a PR.",
    55  		Examples:    []string{"/kind bug", "/remove-area prow", "/sig testing", "/language zh"},
    56  	})
    57  	return pluginHelp, nil
    58  }
    59  
    60  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
    61  	var labels []string
    62  	if pc.PluginConfig.Label != nil {
    63  		labels = pc.PluginConfig.Label.AdditionalLabels
    64  	}
    65  	return handle(pc.GitHubClient, pc.Logger, labels, &e)
    66  }
    67  
    68  type githubClient interface {
    69  	CreateComment(owner, repo string, number int, comment string) error
    70  	AddLabel(owner, repo string, number int, label string) error
    71  	RemoveLabel(owner, repo string, number int, label string) error
    72  	GetRepoLabels(owner, repo string) ([]github.Label, error)
    73  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    74  }
    75  
    76  // Get Labels from Regexp matches
    77  func getLabelsFromREMatches(matches [][]string) (labels []string) {
    78  	for _, match := range matches {
    79  		for _, label := range strings.Split(match[0], " ")[1:] {
    80  			label = strings.ToLower(match[1] + "/" + strings.TrimSpace(label))
    81  			labels = append(labels, label)
    82  		}
    83  	}
    84  	return
    85  }
    86  
    87  // getLabelsFromGenericMatches returns label matches with extra labels if those
    88  // have been configured in the plugin config.
    89  func getLabelsFromGenericMatches(matches [][]string, additionalLabels []string) []string {
    90  	if len(additionalLabels) == 0 {
    91  		return nil
    92  	}
    93  	var labels []string
    94  	for _, match := range matches {
    95  		parts := strings.Split(match[0], " ")
    96  		if ((parts[0] != "/label") && (parts[0] != "/remove-label")) || len(parts) != 2 {
    97  			continue
    98  		}
    99  		for _, l := range additionalLabels {
   100  			if l == parts[1] {
   101  				labels = append(labels, parts[1])
   102  			}
   103  		}
   104  	}
   105  	return labels
   106  }
   107  
   108  func handle(gc githubClient, log *logrus.Entry, additionalLabels []string, e *github.GenericCommentEvent) error {
   109  	labelMatches := labelRegex.FindAllStringSubmatch(e.Body, -1)
   110  	removeLabelMatches := removeLabelRegex.FindAllStringSubmatch(e.Body, -1)
   111  	customLabelMatches := customLabelRegex.FindAllStringSubmatch(e.Body, -1)
   112  	customRemoveLabelMatches := customRemoveLabelRegex.FindAllStringSubmatch(e.Body, -1)
   113  	if len(labelMatches) == 0 && len(removeLabelMatches) == 0 && len(customLabelMatches) == 0 && len(customRemoveLabelMatches) == 0 {
   114  		return nil
   115  	}
   116  
   117  	org := e.Repo.Owner.Login
   118  	repo := e.Repo.Name
   119  
   120  	repoLabels, err := gc.GetRepoLabels(org, repo)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	labels, err := gc.GetIssueLabels(org, repo, e.Number)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	RepoLabelsExisting := map[string]string{}
   130  	for _, l := range repoLabels {
   131  		RepoLabelsExisting[strings.ToLower(l.Name)] = l.Name
   132  	}
   133  	var (
   134  		nonexistent         []string
   135  		noSuchLabelsOnIssue []string
   136  		labelsToAdd         []string
   137  		labelsToRemove      []string
   138  	)
   139  
   140  	// Get labels to add and labels to remove from regexp matches
   141  	labelsToAdd = append(getLabelsFromREMatches(labelMatches), getLabelsFromGenericMatches(customLabelMatches, additionalLabels)...)
   142  	labelsToRemove = append(getLabelsFromREMatches(removeLabelMatches), getLabelsFromGenericMatches(customRemoveLabelMatches, additionalLabels)...)
   143  
   144  	// Add labels
   145  	for _, labelToAdd := range labelsToAdd {
   146  		if github.HasLabel(labelToAdd, labels) {
   147  			continue
   148  		}
   149  
   150  		if _, ok := RepoLabelsExisting[labelToAdd]; !ok {
   151  			nonexistent = append(nonexistent, labelToAdd)
   152  			continue
   153  		}
   154  
   155  		if err := gc.AddLabel(org, repo, e.Number, RepoLabelsExisting[labelToAdd]); err != nil {
   156  			log.WithError(err).Errorf("Github failed to add the following label: %s", labelToAdd)
   157  		}
   158  	}
   159  
   160  	// Remove labels
   161  	for _, labelToRemove := range labelsToRemove {
   162  		if !github.HasLabel(labelToRemove, labels) {
   163  			noSuchLabelsOnIssue = append(noSuchLabelsOnIssue, labelToRemove)
   164  			continue
   165  		}
   166  
   167  		if _, ok := RepoLabelsExisting[labelToRemove]; !ok {
   168  			nonexistent = append(nonexistent, labelToRemove)
   169  			continue
   170  		}
   171  
   172  		if err := gc.RemoveLabel(org, repo, e.Number, labelToRemove); err != nil {
   173  			log.WithError(err).Errorf("Github failed to remove the following label: %s", labelToRemove)
   174  		}
   175  	}
   176  
   177  	//TODO(grodrigues3): Once labels are standardized, make this reply with a comment.
   178  	if len(nonexistent) > 0 {
   179  		log.Infof("Nonexistent labels: %v", nonexistent)
   180  	}
   181  
   182  	// Tried to remove Labels that were not present on the Issue
   183  	if len(noSuchLabelsOnIssue) > 0 {
   184  		msg := fmt.Sprintf(nonExistentLabelOnIssue, strings.Join(noSuchLabelsOnIssue, ", "))
   185  		return gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg))
   186  	}
   187  
   188  	return nil
   189  }