github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/retitle/retitle.go (about)

     1  /*
     2  Copyright 2019 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 retitle implements the retitle plugin
    18  package retitle
    19  
    20  import (
    21  	"regexp"
    22  	"strings"
    23  
    24  	"github.com/sirupsen/logrus"
    25  
    26  	"sigs.k8s.io/prow/pkg/config"
    27  	"sigs.k8s.io/prow/pkg/github"
    28  	"sigs.k8s.io/prow/pkg/pluginhelp"
    29  	"sigs.k8s.io/prow/pkg/plugins"
    30  	"sigs.k8s.io/prow/pkg/plugins/invalidcommitmsg"
    31  	"sigs.k8s.io/prow/pkg/plugins/trigger"
    32  )
    33  
    34  const (
    35  	// pluginName defines this plugin's registered name.
    36  	pluginName = "retitle"
    37  )
    38  
    39  var (
    40  	retitleRe = regexp.MustCompile(`(?mi)^/retitle\s*(.*)$`)
    41  )
    42  
    43  func init() {
    44  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericCommentEvent, helpProvider)
    45  }
    46  
    47  func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
    48  	var configMsg string
    49  	if config.Retitle.AllowClosedIssues {
    50  		configMsg = "The retitle plugin also allows retitling closed/merged issues and PRs."
    51  	} else {
    52  		configMsg = "The retitle plugin does not allow retitling closed/merged issues and PRs."
    53  	}
    54  	yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{
    55  		Retitle: plugins.Retitle{
    56  			AllowClosedIssues: true,
    57  		},
    58  	})
    59  	if err != nil {
    60  		logrus.WithError(err).Warnf("cannot generate comments for %s plugin", pluginName)
    61  	}
    62  	pluginHelp := &pluginhelp.PluginHelp{
    63  		Description: "The retitle plugin allows users to re-title pull requests and issues where GitHub permissions don't allow them to.",
    64  		Config: map[string]string{
    65  			"": configMsg,
    66  		},
    67  		Snippet: yamlSnippet,
    68  	}
    69  	pluginHelp.AddCommand(pluginhelp.Command{
    70  		Usage:       "/retitle <title>",
    71  		Description: "Edits the pull request or issue title.",
    72  		Featured:    true,
    73  		WhoCanUse:   "Collaborators on the repository.",
    74  		Examples:    []string{"/retitle New Title"},
    75  	})
    76  	return pluginHelp, nil
    77  }
    78  
    79  func handleGenericCommentEvent(pc plugins.Agent, e github.GenericCommentEvent) error {
    80  	var (
    81  		org  = e.Repo.Owner.Login
    82  		repo = e.Repo.Name
    83  	)
    84  	return handleGenericComment(pc.GitHubClient, func(user string) (bool, error) {
    85  		t := pc.PluginConfig.TriggerFor(org, repo)
    86  		trustedResponse, err := trigger.TrustedUser(pc.GitHubClient, t.OnlyOrgMembers, t.TrustedApps, t.TrustedOrg, user, org, repo)
    87  		return trustedResponse.IsTrusted, err
    88  	}, pc.PluginConfig.Retitle.AllowClosedIssues, pc.Logger, e)
    89  }
    90  
    91  type githubClient interface {
    92  	CreateComment(owner, repo string, number int, comment string) error
    93  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
    94  	EditPullRequest(org, repo string, number int, pr *github.PullRequest) (*github.PullRequest, error)
    95  	GetIssue(org, repo string, number int) (*github.Issue, error)
    96  	EditIssue(org, repo string, number int, issue *github.Issue) (*github.Issue, error)
    97  }
    98  
    99  func handleGenericComment(gc githubClient, isTrusted func(string) (bool, error), allowClosedIssues bool, log *logrus.Entry, gce github.GenericCommentEvent) error {
   100  	// If closed/merged issues and PRs shouldn't be considered,
   101  	// return early if issue state is not open.
   102  	if !allowClosedIssues && gce.IssueState != "open" {
   103  		return nil
   104  	}
   105  
   106  	// Only consider new comments.
   107  	if gce.Action != github.GenericCommentActionCreated {
   108  		return nil
   109  	}
   110  
   111  	// Make sure they are requesting a re-title
   112  	if !retitleRe.MatchString(gce.Body) {
   113  		return nil
   114  	}
   115  
   116  	var (
   117  		org    = gce.Repo.Owner.Login
   118  		repo   = gce.Repo.Name
   119  		number = gce.Number
   120  		user   = gce.User.Login
   121  	)
   122  
   123  	trusted, err := isTrusted(user)
   124  	if err != nil {
   125  		log.WithError(err).Error("Could not check if user was trusted.")
   126  		return err
   127  	}
   128  	if !trusted {
   129  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(gce.Body, gce.HTMLURL, user, `Re-titling can only be requested by trusted users, like repository collaborators.`))
   130  	}
   131  
   132  	matches := retitleRe.FindStringSubmatch(gce.Body)
   133  	if matches == nil {
   134  		// this shouldn't happen since we checked above
   135  		return nil
   136  	}
   137  	newTitle := strings.TrimSpace(matches[1])
   138  	if newTitle == "" {
   139  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(gce.Body, gce.HTMLURL, user, `Titles may not be empty.`))
   140  	}
   141  
   142  	if invalidcommitmsg.AtMentionRegex.MatchString(newTitle) || invalidcommitmsg.CloseIssueRegex.MatchString(newTitle) {
   143  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(gce.Body, gce.HTMLURL, user, `Titles may not contain [keywords](https://help.github.com/articles/closing-issues-using-keywords) which can automatically close issues and at(@) mentions.`))
   144  	}
   145  
   146  	if gce.IsPR {
   147  		pr, err := gc.GetPullRequest(org, repo, number)
   148  		if err != nil {
   149  			return err
   150  		}
   151  		pr.Title = newTitle
   152  		_, err = gc.EditPullRequest(org, repo, number, pr)
   153  		return err
   154  	}
   155  	issue, err := gc.GetIssue(org, repo, number)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	issue.Title = newTitle
   160  	_, err = gc.EditIssue(org, repo, number, issue)
   161  	return err
   162  }