Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- rule for approvers
- merge on command
- update branch
- delete stale branches


### Commands
Expand Down Expand Up @@ -44,23 +45,29 @@ go build ./

## Config file

Config file must be named `.mrbot.yaml` and placed in root directory
Config file must be named `.mrbot.yaml`, placed in root directory, default branch (main/master)

- `approvers`: list of users who must approve MR/PR, default is empty (`[]`)
```yaml
approvers: [] # list of users who must approve MR/PR, default is empty ([])

- `min_approvals`: minimum number of required approvals, default is `1`
min_approvals: 1 # minimum number of required approvals, default is 1

- `allow_empty_description`: whether MR description is allowed to be empty or not, default is `true`
allow_empty_description: true # whether MR description is allowed to be empty or not, default is true

- `allow_failing_pipelines`: whether pipelines are allowed to fail, default is `true`
allow_failing_pipelines: true # whether pipelines are allowed to fail, default is true

- `title_regex`: pattern of title, default is `.*`
title_regex: ".*" # pattern of title, default is ".*"

- `greetings_enabled`: enable message for new MR, default is `false`
greetings:
enabled: false # enable message for new MR, default is false
template: "" # template of message for new MR, default is "Requirements:\n - Min approvals: {{ .MinApprovals }}\n - Title regex: {{ .TitleRegex }}\n\nOnce you've done, send **!merge** command and i will merge it!"

- `greetings_template`: template of message for new MR, default is `Requirements:\n - Min approvals: {{ .MinApprovals }}\n - Title regex: {{ .TitleRegex }}\n\nOnce you've done, send **!merge** command and i will merge it!`
auto_master_merge: false # the bot tries to update branch from master, default is false

- `auto_master_merge`: the bot tries to merge target branch, default is `false`
stale_branches_deletion:
enabled: false # enable deletion of stale branches after every merge, default is false
days: 90 # branch is staled after int days, default is 90
```

Example:

Expand All @@ -73,9 +80,13 @@ allow_empty_description: true
allow_failing_pipelines: true
allow_failing_tests: true
title_regex: "^[A-Z]+-[0-9]+" # title begins with jira key prefix, e.g. SCO-123 My cool Title
greetings_enabled: true
greetings_template: "Requirements:\n - Min approvals: {{ .MinApprovals }}\n - Title regex: {{ .TitleRegex }}\n\nOnce you've done, send **!merge** command and i will merge it!"
greetings:
enabled: true
template: "Requirements:\n - Min approvals: {{ .MinApprovals }}\n - Title regex: {{ .TitleRegex }}\n\nOnce you've done, send **!merge** command and i will merge it!"
auto_master_merge: true
stale_branches_deletion:
enabled: true
days: 90
```

place it in root of your repo and name it `.mrbot.yaml`
22 changes: 22 additions & 0 deletions handlers/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,28 @@ func (g *GitlabProvider) GetMRInfo(projectId, mergeId int, configPath string) (*
return &info, nil
}

func (g *GitlabProvider) ListBranches(projectId int) ([]handlers.Branch, error) {
branches, _, err := g.client.Branches.ListBranches(projectId, &gitlab.ListBranchesOptions{})
if err != nil {
return nil, err
}

staleBranches := []handlers.Branch{}
for _, b := range branches {
if b.Default || b.Protected {
continue
}

staleBranches = append(staleBranches, handlers.Branch{Name: b.Name, LastUpdated: *b.Commit.CreatedAt})
}
return staleBranches, nil
}

func (g *GitlabProvider) DeleteBranch(projectId int, name string) error {
_, err := g.client.Branches.DeleteBranch(projectId, name)
return err
}

func New() handlers.RequestProvider {
var err error
var p GitlabProvider
Expand Down
18 changes: 13 additions & 5 deletions handlers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type RequestProvider interface {
LeaveComment(projectId, mergeId int, message string) error
GetMRInfo(projectId, mergeId int, path string) (*MrInfo, error)
UpdateFromMaster(projectId, mergeId int) error
ListBranches(projectId int) ([]Branch, error)
DeleteBranch(projectId int, name string) error
}

type Config struct {
Expand All @@ -55,14 +57,20 @@ type Config struct {
AllowFailingTests bool `yaml:"allow_failing_tests"`
TitleRegex string `yaml:"title_regex"`
AllowEmptyDescription bool `yaml:"allow_empty_description"`
EnableGreetings bool `yaml:"greetings_enabled"`
GreetingsTemplate string `yaml:"greetings_template"`
AutoMasterMerge bool `yaml:"auto_master_merge"`
Greetings struct {
Enabled bool `yaml:"enabled"`
Template string `yaml:"template"`
} `yaml:"greetings"`
AutoMasterMerge bool `yaml:"auto_master_merge"`
StaleBranchesDeletion struct {
Enabled bool `yaml:"enabled"`
Days int `yaml:"days"`
} `yaml:"stale_branches_deletion"`
}

func New(providerName string) (*Request, error) {
providersMu.Lock()
defer providersMu.Unlock()
providersMu.RLock()
defer providersMu.RUnlock()

if _, ok := providers[providerName]; !ok {
return nil, &Error{text: "Provider can't be nil"}
Expand Down
26 changes: 21 additions & 5 deletions handlers/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,21 @@ func (r *Request) ParseConfig(content string) (*Config, error) {
AllowFailingTests: true,
TitleRegex: ".*",
AllowEmptyDescription: true,
EnableGreetings: false,
GreetingsTemplate: "Requirements:\n - Min approvals: {{ .MinApprovals }}\n - Title regex: {{ .TitleRegex }}\n\nOnce you've done, send **!merge** command and i will merge it!",
AutoMasterMerge: false,
Greetings: struct {
Enabled bool `yaml:"enabled"`
Template string `yaml:"template"`
}{
Enabled: false,
Template: "Requirements:\n - Min approvals: {{ .MinApprovals }}\n - Title regex: {{ .TitleRegex }}\n\nOnce you've done, send **!merge** command and i will merge it!",
},
AutoMasterMerge: false,
StaleBranchesDeletion: struct {
Enabled bool `yaml:"enabled"`
Days int `yaml:"days"`
}{
Enabled: false,
Days: 90,
},
}

if err := yaml.Unmarshal([]byte(content), mrConfig); err != nil {
Expand All @@ -84,11 +96,11 @@ func (r *Request) Greetings(projectId, id int) error {
return err
}

if !r.config.EnableGreetings {
if !r.config.Greetings.Enabled {
return nil
}

tmpl, err := template.New("greetings").Parse(r.config.GreetingsTemplate)
tmpl, err := template.New("greetings").Parse(r.config.Greetings.Template)
if err != nil {
return err
}
Expand All @@ -111,6 +123,10 @@ func (r *Request) Merge(projectId, id int) (bool, string, error) {
_ = r.provider.UpdateFromMaster(projectId, id)
}

if r.config.StaleBranchesDeletion.Enabled {
defer r.cleanStaleBranches(projectId)
}

if ok, text, err := r.IsValid(projectId, id); ok {
if err := r.provider.Merge(projectId, id, fmt.Sprintf("%s\nMerged by MergeApproveBot", r.info.Title)); err != nil {
return false, "", err
Expand Down
8 changes: 8 additions & 0 deletions handlers/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ func (p *testProvider) Merge(projectId, id int, message string) error {
return p.err
}

func (p *testProvider) ListBranches(projectId int) ([]Branch, error) {
return nil, nil
}

func (p *testProvider) DeleteBranch(projectId int, name string) error {
return nil
}

func (p *testProvider) GetMRInfo(projectId, id int, path string) (*MrInfo, error) {
return &MrInfo{
Title: p.title,
Expand Down
33 changes: 33 additions & 0 deletions handlers/stalebranches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package handlers

import (
"log/slog"
"time"
)

type Branch struct {
Name string
LastUpdated time.Time
}

func (r *Request) cleanStaleBranches(projectId int) {
candidates, err := r.provider.ListBranches(projectId)
if err != nil {
slog.Error("ListBranches returns error", "err", err)
return
}

days := r.config.StaleBranchesDeletion.Days
for _, b := range candidates {
now := time.Now()
span := now.Sub(b.LastUpdated)
if span > time.Duration(time.Duration(days)*24*time.Hour) {
// branch is stale
// delete branch
slog.Debug("branch info", "name", b.Name, "createdAt", b.LastUpdated.String())
if err := r.provider.DeleteBranch(projectId, b.Name); err != nil {
slog.Error("DeleteBranch returns error", "branch", b.Name, "err", err)
}
}
}
}
Loading