diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..16c697e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +testdata +*_test.go diff --git a/approve.go b/approve.go new file mode 100644 index 0000000..9fa761c --- /dev/null +++ b/approve.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "regexp" + + "github.com/sirupsen/logrus" +) + +const approvedLabel = "approved" + +var ( + regAddApprove = regexp.MustCompile(`(?mi)^/approve\s*$`) + regRemoveApprove = regexp.MustCompile(`(?mi)^/approve cancel\s*$`) +) + +func (bot *robot) handleApprove(configmap *repoConfig, comment, commenter, author, org, repo, number string) error { + if regAddApprove.MatchString(comment) { + return bot.AddApprove(commenter, author, org, repo, number, configmap.LgtmCountsRequired) + } + + if regRemoveApprove.MatchString(comment) { + return bot.removeApprove(commenter, author, org, repo, number, configmap.LgtmCountsRequired) + } + + return nil +} + +func (bot *robot) AddApprove(commenter, author, org, repo, number string, lgtmCounts uint) error { + logrus.Infof("AddApprove, commenter: %s, author: %s, org: %s, repo: %s, number: %s", commenter, author, org, repo, number) + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + if ok := bot.cli.AddPRLabels(org, repo, number, []string{approvedLabel}); !ok { + return fmt.Errorf("failed to add label on pull request") + } + if ok := bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentAddLabel, approvedLabel, commenter)); !ok { + return fmt.Errorf("failed to comment on pull request") + } + } else if !pass { + bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentNoPermissionForLgtmLabel, commenter)) + } else { + return fmt.Errorf("failed to add label on pull request") + } + return nil +} + +func (bot *robot) removeApprove(commenter, author, org, repo, number string, lgtmCounts uint) error { + logrus.Infof("removeApprove, commenter: %s, author: %s, org: %s, repo: %s, number: %s", commenter, author, org, repo, number) + + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + bot.cli.RemovePRLabels(org, repo, number, []string{approvedLabel}) + bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentRemovedLabel, approvedLabel, commenter)) + } else if !pass { + bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentNoPermissionForLabel, commenter, "remove", approvedLabel)) + } else { + return fmt.Errorf("failed to remove label on pull request") + } + + return nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..eb53b10 --- /dev/null +++ b/config.go @@ -0,0 +1,155 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "errors" + "fmt" + + "github.com/opensourceways/server-common-lib/config" +) + +// configuration holds a list of repoConfig configurations. +type configuration struct { + ConfigItems []repoConfig `json:"config_items,omitempty"` + UserMarkFormat string `json:"user_mark_format,omitempty"` + PlaceholderCommenter string `json:"placeholder_commenter"` +} + +// Validate to check the configmap data's validation, returns an error if invalid +func (c *configuration) Validate() error { + if c == nil { + return errors.New("configuration is nil") + } + + // Validate each repo configuration + items := c.ConfigItems + for i := range items { + if err := items[i].validate(); err != nil { + return err + } + } + + return nil +} + +// get retrieves a repoConfig for a given organization and repository. +// Returns the repoConfig if found, otherwise returns nil. +func (c *configuration) get(org, repo string) *repoConfig { + if c == nil || len(c.ConfigItems) == 0 { + return nil + } + + for i := range c.ConfigItems { + ok, _ := c.ConfigItems[i].RepoFilter.CanApply(org, org+"/"+repo) + if ok { + return &c.ConfigItems[i] + } + } + + return nil +} + +// repoConfig is a configuration struct for a organization and repository. +// It includes a RepoFilter and a boolean value indicating if an issue can be closed only when its linking PR exists. +type repoConfig struct { + // RepoFilter is used to filter repositories. + config.RepoFilter + // LegalOperator means who can add or remove labels legally + LegalOperator string `json:"legal_operator,omitempty"` + + // LgtmCountsRequired specifies the number of lgtm label which will be need for the pr. + // When it is greater than 1, the lgtm label is composed of 'lgtm-login'. + // The default value is 1 which means the lgtm label is itself. + LgtmCountsRequired uint `json:"lgtm_counts_required,omitempty"` + + // LabelsForMerge specifies the labels except approved and lgtm relevant labels + // that must be available to merge pr + LabelsForMerge []string `json:"labels_for_merge,omitempty"` + + // LabelsNotAllowMerge means that if pull request has these labels, it can not been merged + // even all conditions are met + LabelsNotAllowMerge []string `json:"labels_not_allow_merge,omitempty"` + + // MergeMethod is the method to merge PR. + // The default method of merge. Valid options are squash and merge. + MergeMethod string `json:"merge_method,omitempty"` +} + +type freezeFile struct { + Owner string `json:"owner" required:"true"` + Repo string `json:"repo" required:"true"` + Branch string `json:"branch" required:"true"` + Path string `json:"path" required:"true"` +} + +type branchKeeper struct { + Owner string `json:"owner" required:"true"` + Repo string `json:"repo" required:"true"` + Branch string `json:"branch" required:"true"` +} + +func (b branchKeeper) validate() error { + if b.Owner == "" { + return fmt.Errorf("missing owner of branch keeper") + } + + if b.Repo == "" { + return fmt.Errorf("missing repo of branch keeper") + } + + if b.Branch == "" { + return fmt.Errorf("missing branch of branch keeper") + } + + return nil +} + +func (f freezeFile) validate() error { + if f.Owner == "" { + return fmt.Errorf("missing owner of freeze file") + } + + if f.Repo == "" { + return fmt.Errorf("missing repo of freeze file") + } + + if f.Branch == "" { + return fmt.Errorf("missing branch of freeze file") + } + + if f.Path == "" { + return fmt.Errorf("missing path of freeze file") + } + + return nil +} + +// validate to check the repoConfig data's validation, returns an error if invalid +func (c *repoConfig) validate() error { + // If the bot is not configured to monitor any repositories, return an error. + if len(c.Repos) == 0 { + return errors.New("the repositories configuration can not be empty") + } + + return c.RepoFilter.Validate() +} + +type litePRCommiter struct { + // Email is the one of committer in a commit when a PR is lite + Email string `json:"email" required:"true"` + + // Name is the one of committer in a commit when a PR is lite + Name string `json:"name" required:"true"` +} diff --git a/lgtm.go b/lgtm.go new file mode 100644 index 0000000..70c2d5b --- /dev/null +++ b/lgtm.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "regexp" + "strings" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/sets" +) + +const ( + // the gitee platform limits the maximum length of label to 20. + labelLenLimit = 20 + lgtmLabel = "lgtm" + + commentAddLGTMBySelf = "***lgtm*** can not be added in your self-own pull request. :astonished:" + commentClearLabelCaseByPRUpdate = `New code changes of pr are detected and remove these labels ***%s***. :flushed: ` + commentClearLabelCaseByReopenPR = `When PR is reopened, remove these labels ***%s***. :flushed: ` + commentNoPermissionForLgtmLabel = `Thanks for your review, ***%s***, your opinion is very important to us.:wave: +The maintainers will consider your advice carefully.` + commentNoPermissionForLabel = ` +***@%s*** has no permission to %s ***%s*** label in this pull request. :astonished: +Please contact to the collaborators in this repository.` + commentAddLabel = `***%s*** was added to this pull request by: ***%s***. :wave: +**NOTE:** If this pull request is not merged while all conditions are met, comment "/check-pr" to try again. :smile: ` + commentRemovedLabel = `***%s*** was removed in this pull request by: ***%s***. :flushed: ` +) + +var ( + regAddLgtm = regexp.MustCompile(`(?mi)^/lgtm\s*$`) + regRemoveLgtm = regexp.MustCompile(`(?mi)^/lgtm cancel\s*$`) +) + +func (bot *robot) handleLGTM(configmap *repoConfig, comment, commenter, author, org, repo, number string) error { + if regAddLgtm.MatchString(comment) { + return bot.addLGTM(commenter, author, org, repo, number, configmap.LgtmCountsRequired) + } + + if regRemoveLgtm.MatchString(comment) { + return bot.removeLGTM(commenter, author, org, repo, number, configmap.LgtmCountsRequired) + } + + return nil +} + +func (bot *robot) addLGTM(commenter, author, org, repo, number string, lgtmCounts uint) error { + logrus.Infof("addLGTM, commenter: %s, author: %s, org: %s, repo: %s, number: %s", commenter, author, org, repo, number) + if author == commenter { + if ok := bot.cli.CreatePRComment(org, repo, number, commentAddLGTMBySelf); !ok { + return fmt.Errorf("failed to comment on pull request") + } + return nil + } + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + label := genLGTMLabel(commenter, lgtmCounts) + + if ok := bot.cli.AddPRLabels(org, repo, number, []string{label}); !ok { + return fmt.Errorf("failed to add label on pull request") + } + if ok := bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentAddLabel, label, commenter)); !ok { + return fmt.Errorf("failed to comment on pull request") + } + } else { + bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentNoPermissionForLgtmLabel, commenter)) + } + return nil + +} + +func (bot *robot) removeLGTM(commenter, author, org, repo, number string, lgtmCounts uint) error { + logrus.Infof("removeLGTM, commenter: %s, author: %s, org: %s, repo: %s, number: %s", commenter, author, org, repo, number) + if author == commenter { + bot.cli.RemovePRLabels(org, repo, number, getLGTMLabelsOnPR(bot.getPRLabelSet(org, repo, number))) + } else { + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + label := genLGTMLabel(commenter, lgtmCounts) + bot.cli.RemovePRLabels(org, repo, number, []string{label}) + bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentRemovedLabel, label, commenter)) + } else { + bot.cli.CreatePRComment(org, repo, number, fmt.Sprintf(commentNoPermissionForLabel, commenter, "remove", lgtmLabel)) + } + + } + return nil +} + +func genLGTMLabel(commenter string, lgtmCount uint) string { + if lgtmCount <= 1 { + return lgtmLabel + } + + l := fmt.Sprintf("%s-%s", lgtmLabel, strings.ToLower(commenter)) + if len(l) > labelLenLimit { + return l[:labelLenLimit] + } + + return l +} + +func getLGTMLabelsOnPR(labels sets.Set[string]) []string { + var r []string + + for l := range labels { + if strings.HasPrefix(l, lgtmLabel) { + r = append(r, l) + } + } + + return r +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f77a887 --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "flag" + "github.com/opensourceways/robot-framework-lib/framework" + "os" +) + +const component = "robot-universal-review" + +func main() { + + opt := new(robotOptions) + // Gather the necessary arguments from command line for project startup + cnf, token := opt.gatherOptions(flag.NewFlagSet(os.Args[0], flag.ExitOnError), os.Args[1:]...) + if opt.interrupt { + return + } + + bot := newRobot(cnf, token) + framework.StartupServer(framework.NewServer(bot, opt.service), opt.service) +} diff --git a/merge.go b/merge.go new file mode 100644 index 0000000..8f65429 --- /dev/null +++ b/merge.go @@ -0,0 +1,143 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/opensourceways/robot-framework-lib/client" + "k8s.io/apimachinery/pkg/util/sets" +) + +const ( + msgPRConflicts = "PR conflicts to the target branch." + msgMissingLabels = "PR does not have these lables: %s" + msgInvalidLabels = "PR should remove these labels: %s" + msgNotEnoughLGTMLabel = "PR needs %d lgtm labels and now gets %d" + ActionAddLabel = "add label" +) + +type labelLog struct { + label string + who string + t time.Time +} + +func (bot *robot) handleMerge(configmap *repoConfig, org, repo, number string) error { + labels := bot.getPRLabelSet(org, repo, number) + ops, ok := bot.cli.ListPullRequestOperationLogs(org, repo, number) + if !ok { + return fmt.Errorf("failed to list pull request operation logs") + } + if err := checkLabelsLegal(configmap, ops, labels); err != nil { + return err + } + if reasons := isLabelMatched(configmap, labels); len(reasons) > 0 { + return fmt.Errorf(strings.Join(reasons, "\n\n")) + } + + methodOfMerge := bot.genMergeMethod(org, repo, number) + if ok := bot.cli.MergePullRequest(org, repo, number, methodOfMerge); !ok { + return fmt.Errorf("failed to merge pull request") + } + return nil +} + +func isLabelMatched(configmap *repoConfig, labels sets.Set[string]) []string { + var reasons []string + for _, l := range configmap.LabelsNotAllowMerge { + if labels.Has(l) { + reasons = append(reasons, fmt.Sprintf(msgInvalidLabels, l)) + } + } + + needs := sets.New[string](approvedLabel) + needs.Insert(configmap.LabelsForMerge...) + + if ln := configmap.LgtmCountsRequired; ln == 1 { + needs.Insert(lgtmLabel) + } else { + v := getLGTMLabelsOnPR(labels) + if n := uint(len(v)); n < ln { + reasons = append(reasons, fmt.Sprintf(msgNotEnoughLGTMLabel, ln, n)) + } + } + + if v := needs.Difference(labels); v.Len() > 0 { + vl := v.UnsortedList() + var vlp []string + for _, i := range vl { + vlp = append(vlp, fmt.Sprintf("***%s***", i)) + } + reasons = append(reasons, fmt.Sprintf(msgMissingLabels, strings.Join(vlp, ", "))) + } + return reasons +} + +func checkLabelsLegal(configmap *repoConfig, ops []client.PullRequestOperationLog, labels sets.Set[string]) error { + reason := make([]string, 0, len(labels)) + needs := sets.New[string](approvedLabel) + needs.Insert(configmap.LabelsForMerge...) + if ln := configmap.LgtmCountsRequired; ln == 1 { + needs.Insert(lgtmLabel) + } else { + needs.Insert(getLGTMLabelsOnPR(labels)...) + } + legalOperator := configmap.LegalOperator + for label := range labels { + if ok := needs.Has(label); ok { + if s := isLabelLegal(ops, label, legalOperator); s != "" { + reason = append(reason, s) + } + } + } + if n := len(reason); n > 0 { + s := "label is " + if n > 1 { + s = "labels are " + } + return fmt.Errorf("**The following %s not ready**.\n\n%s", s, strings.Join(reason, "\n\n")) + } + return nil +} + +func isLabelLegal(ops []client.PullRequestOperationLog, label string, legalOperator string) string { + labelLog, ok := getLatestLog(ops, label) + if !ok { + return fmt.Sprintf("The corresponding operation log is missing. you should delete "+ + "the label **%s** and add it again by correct way", label) + } + if labelLog.who != legalOperator { + return fmt.Sprintf("%s You can't add **%s** by yourself, you should delete "+ + "the label and add it again by correct way", labelLog.who, labelLog.label) + } + return "" +} + +func getLatestLog(ops []client.PullRequestOperationLog, label string) (labelLog, bool) { + var t time.Time + index := -1 + + for i := range ops { + op := &ops[i] + if !strings.HasPrefix(op.Content, ActionAddLabel) || !strings.Contains(op.Content, label) { + continue + } + + if index < 0 || op.CreatedAt.After(t) { + t = op.CreatedAt + index = i + } + } + + if index >= 0 { + if user := ops[index].UserName; user != "" { + return labelLog{ + label: label, + t: t, + who: user, + }, true + } + } + return labelLog{}, false +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..dab5749 --- /dev/null +++ b/options.go @@ -0,0 +1,80 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "flag" + "os" + + "github.com/opensourceways/robot-framework-lib/config" + "github.com/opensourceways/server-common-lib/secret" + "github.com/sirupsen/logrus" +) + +type robotOptions struct { + service config.FrameworkOptions + delToken bool + interrupt bool + tokenPath string +} + +func (o *robotOptions) addFlags(fs *flag.FlagSet) { + o.service.AddFlagsComposite(fs) + fs.StringVar( + &o.tokenPath, "token-path", "", + "Path to the file containing the token secret.", + ) + fs.BoolVar( + &o.delToken, "del-token", true, + "An flag to delete token secret file.", + ) +} + +func (o *robotOptions) validateFlags() (*configuration, []byte) { + if err := o.service.ValidateComposite(); err != nil { + logrus.Errorf("invalid service options, err:%s", err.Error()) + o.interrupt = true + return nil, nil + } + + configmap, err := config.NewConfigmapAgent(&configuration{}, o.service.ConfigFile) + if err != nil { + logrus.Errorf("load config, err:%s", err.Error()) + return nil, nil + } + + token, err := secret.LoadSingleSecret(o.tokenPath) + if err != nil { + logrus.WithError(err).Error("fatal error occurred while loading token") + o.interrupt = true + } + if o.delToken { + if err = os.Remove(o.tokenPath); err != nil { + logrus.WithError(err).Error("fatal error occurred while deleting token") + o.interrupt = true + } + } + + return configmap.GetConfigmap().(*configuration), token +} + +// gatherOptions gather the necessary arguments from command line for project startup. +// It returns the configuration and the token to using for subsequent processes. +func (o *robotOptions) gatherOptions(fs *flag.FlagSet, args ...string) (*configuration, []byte) { + o.addFlags(fs) + _ = fs.Parse(args) + cnf, token := o.validateFlags() + + return cnf, token +} diff --git a/rebase.go b/rebase.go new file mode 100644 index 0000000..908e57f --- /dev/null +++ b/rebase.go @@ -0,0 +1,50 @@ +package main + +import ( + "net/url" + "regexp" + + "github.com/sirupsen/logrus" +) + +var ( + regAddRebase = regexp.MustCompile(`(?mi)^/rebase\s*$`) + regRemoveRebase = regexp.MustCompile(`(?mi)^/rebase cancel\s*$`) +) + +const rebaseLabel = "merge/rebase" + +func (bot *robot) handleRebase(comment, commenter, org, repo, number string) error { + if regAddRebase.MatchString(comment) { + return bot.addRebase(commenter, org, repo, number) + } + + if regRemoveRebase.MatchString(comment) { + return bot.removeRebase(commenter, org, repo, number) + } + + return nil +} + +func (bot *robot) addRebase(commenter, org, repo, number string) error { + logrus.Infof("addRebase, commenter: %s, org: %s, repo: %s, number: %s", commenter, org, repo, number) + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + label := bot.getPRLabelSet(org, repo, number) + if _, ok := label["merge/squash"]; ok { + bot.cli.CreatePRComment(org, repo, number, + "Please use **/squash cancel** to remove **merge/squash** label, and try **/rebase** again") + return nil + } + bot.cli.AddPRLabels(org, repo, number, []string{rebaseLabel}) + } + return nil + +} + +func (bot *robot) removeRebase(commenter, org, repo, number string) error { + logrus.Infof("removeRebase, commenter: %s, org: %s, repo: %s, number: %s", commenter, org, repo, number) + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + bot.cli.RemovePRLabels(org, repo, number, []string{url.QueryEscape(rebaseLabel)}) + } + return nil +} diff --git a/robot.go b/robot.go new file mode 100644 index 0000000..928fe70 --- /dev/null +++ b/robot.go @@ -0,0 +1,142 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "errors" + "strings" + + "github.com/opensourceways/robot-framework-lib/client" + "github.com/opensourceways/robot-framework-lib/config" + "github.com/opensourceways/robot-framework-lib/framework" + "github.com/opensourceways/robot-framework-lib/utils" + "github.com/sirupsen/logrus" +) + +// iClient is an interface that defines methods for client-side interactions +type iClient interface { + // CreatePRComment creates a comment for a pull request in a specified organization and repository + CreatePRComment(org, repo, number, comment string) (success bool) + + AddPRLabels(org, repo, number string, labels []string) (success bool) + RemovePRLabels(org, repo, number string, labels []string) (success bool) + GetPullRequestCommits(org, repo, number string) (result []client.PRCommit, success bool) + ListPullRequestComments(org, repo, number string) (result []client.PRComment, success bool) + DeletePRComment(org, repo, commentID string) (success bool) + CheckCLASignature(urlStr string) (signState string, success bool) + CheckIfPRCreateEvent(evt *client.GenericEvent) (yes bool) + CheckIfPRSourceCodeUpdateEvent(evt *client.GenericEvent) (yes bool) + CheckPermission(org, repo, username string) (pass, success bool) + GetPullRequestLabels(org, repo, number string) (result []string, success bool) + MergePullRequest(org, repo, number, mergeMethod string) (success bool) + CheckIfPRReopenEvent(evt *client.GenericEvent) (yes bool) + CheckIfPRLabelsUpdateEvent(evt *client.GenericEvent) (yes bool) + ListPullRequestOperationLogs(org, repo, number string) (result []client.PullRequestOperationLog, success bool) +} + +type robot struct { + cli iClient + cnf *configuration + log *logrus.Entry +} + +func (bot *robot) GetConfigmap() config.Configmap { + return bot.cnf +} + +func newRobot(c *configuration, token []byte) *robot { + logger := framework.NewLogger().WithField("component", component) + return &robot{cli: client.NewClient(token, logger), cnf: c, log: logger} +} + +func (bot *robot) NewConfig() config.Configmap { + return &configuration{} +} + +func (bot *robot) RegisterEventHandler(p framework.HandlerRegister) { + p.RegisterPullRequestHandler(bot.handlePREvent) + p.RegisterPullRequestCommentHandler(bot.handlePullRequestCommentEvent) +} + +func (bot *robot) GetLogger() *logrus.Entry { + return bot.log +} + +// getConfig first checks if the specified organization and repository is available in the provided repoConfig list. +// Returns an error if not found the available repoConfig. +func (bot *robot) getConfig(cnf config.Configmap, org, repo string) (*repoConfig, error) { + c := cnf.(*configuration) + if bc := c.get(org, repo); bc != nil { + return bc, nil + } + + return nil, errors.New("no config for this repo: " + org + "/" + repo) +} + +func (bot *robot) handlePREvent(evt *client.GenericEvent, cnf config.Configmap, logger *logrus.Entry) { + org, repo, number := utils.GetString(evt.Org), utils.GetString(evt.Repo), utils.GetString(evt.Number) + repoCnf, err := bot.getConfig(cnf, org, repo) + // If the specified repository not match any repository in the repoConfig list, it logs the error and returns + if err != nil { + logger.WithError(err).Warning() + return + } + + if bot.cli.CheckIfPRReopenEvent(evt) || bot.cli.CheckIfPRSourceCodeUpdateEvent(evt) { + if err := bot.clearLabel(evt, org, repo, number); err != nil { + logger.WithError(err).Warning() + return + } + } + if bot.cli.CheckIfPRLabelsUpdateEvent(evt) { + if err := bot.handleMerge(repoCnf, org, repo, number); err != nil { + logger.WithError(err).Warning() + return + } + } +} + +func (bot *robot) handlePullRequestCommentEvent(evt *client.GenericEvent, cnf config.Configmap, logger *logrus.Entry) { + org, repo, number := utils.GetString(evt.Org), utils.GetString(evt.Repo), utils.GetString(evt.Number) + comment, commenter, author := utils.GetString(evt.Comment), utils.GetString(evt.Commenter), utils.GetString(evt.Author) + repoCnf, err := bot.getConfig(cnf, org, repo) + // If the specified repository not match any repository in the repoConfig list, it logs the error and returns + if err != nil { + logger.WithError(err).Warning() + return + } + lines := strings.Split(comment, "\n") + for _, line := range lines { + if err := bot.handleRebase(line, commenter, org, repo, number); err != nil { + logger.WithError(err).Warning() + } + + if err := bot.handledSquash(line, commenter, org, repo, number); err != nil { + logger.WithError(err).Warning() + } + + if err := bot.handleLGTM(repoCnf, line, commenter, author, org, repo, number); err != nil { + logger.WithError(err).Warning() + } + + if err := bot.handleApprove(repoCnf, line, commenter, author, org, repo, number); err != nil { + logger.WithError(err).Warning() + } + + if err := bot.handleCheckPR(repoCnf, line, commenter, org, repo, number); err != nil { + logger.WithError(err).Warning() + } + } + +} diff --git a/squash.go b/squash.go new file mode 100644 index 0000000..2937553 --- /dev/null +++ b/squash.go @@ -0,0 +1,50 @@ +package main + +import ( + "net/url" + "regexp" + + "github.com/sirupsen/logrus" +) + +var ( + regAddSquash = regexp.MustCompile(`(?mi)^/squash\s*$`) + regRemoveSquash = regexp.MustCompile(`(?mi)^/squash cancel\s*$`) +) + +const squashLabel = "merge/squash" + +func (bot *robot) handledSquash(comment, commenter, org, repo, number string) error { + if regAddSquash.MatchString(comment) { + return bot.addSquash(commenter, org, repo, number) + } + + if regRemoveSquash.MatchString(comment) { + return bot.removedSquash(commenter, org, repo, number) + } + + return nil +} + +func (bot *robot) addSquash(commenter, org, repo, number string) error { + logrus.Infof("addSquash, commenter: %s, org: %s, repo: %s, number: %s", commenter, org, repo, number) + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + label := bot.getPRLabelSet(org, repo, number) + if _, ok := label[rebaseLabel]; ok { + bot.cli.CreatePRComment(org, repo, number, + "Please use **/rebase cancel** to remove **merge/rebase** label, and try **/squash** again") + return nil + } + bot.cli.AddPRLabels(org, repo, number, []string{squashLabel}) + } + return nil + +} + +func (bot *robot) removedSquash(commenter, org, repo, number string) error { + logrus.Infof("removedSquash, commenter: %s, org: %s, repo: %s, number: %s", commenter, org, repo, number) + if pass, ok := bot.cli.CheckPermission(org, repo, commenter); pass && ok { + bot.cli.RemovePRLabels(org, repo, number, []string{url.QueryEscape(squashLabel)}) + } + return nil +}