From 3f64366dd2046135bcec9fe0591e8831a6782d80 Mon Sep 17 00:00:00 2001 From: LiYanghang00 <2623197090@qq.com> Date: Fri, 13 Dec 2024 18:29:30 +0800 Subject: [PATCH] init --- .dockerignore | 3 + approve.go | 59 +++++++++++++++++++ config.go | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++ lgtm.go | 111 ++++++++++++++++++++++++++++++++++++ main.go | 35 ++++++++++++ merge.go | 143 ++++++++++++++++++++++++++++++++++++++++++++++ options.go | 80 ++++++++++++++++++++++++++ rebase.go | 50 ++++++++++++++++ robot.go | 142 +++++++++++++++++++++++++++++++++++++++++++++ squash.go | 50 ++++++++++++++++ 10 files changed, 828 insertions(+) create mode 100644 .dockerignore create mode 100644 approve.go create mode 100644 config.go create mode 100644 lgtm.go create mode 100644 main.go create mode 100644 merge.go create mode 100644 options.go create mode 100644 rebase.go create mode 100644 robot.go create mode 100644 squash.go 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 +}