0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-06-02 17:39:39 +02:00

feat: add validate-commit-message action

This commit is contained in:
Beatriz Vieira 2020-08-27 22:57:55 -03:00
parent 943487fae8
commit 67cd90c762
7 changed files with 218 additions and 8 deletions

View File

@ -8,14 +8,16 @@ import (
// Config env vars for cli configuration
type Config struct {
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,docs,fix,perf,refactor,style,test"`
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE:,BREAKING CHANGES:"`
IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira:,JIRA:,Jira:"`
TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,docs,fix,perf,refactor,style,test"`
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE:,BREAKING CHANGES:"`
IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira:,JIRA:,Jira:"`
TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"`
CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"`
}
func loadConfig() Config {

View File

@ -3,6 +3,8 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sort"
"sv4git/sv"
"time"
@ -228,3 +230,55 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
return nil
}
}
func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.ValidateMessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error {
branch := git.Branch()
if validateMessageProcessor.SkipBranch(branch) {
warn("commit message validation skipped, branch in ignore list...")
return nil
}
filepath := fmt.Sprintf("%s/%s", c.String("path"), c.String("file"))
commitMessage, err := readFile(filepath)
if err != nil {
return fmt.Errorf("failed to read commit message, error: %s", err.Error())
}
if err := validateMessageProcessor.Validate(commitMessage); err != nil {
return fmt.Errorf("invalid commit message, error: %s", err.Error())
}
msg, err := validateMessageProcessor.Enhance(branch, commitMessage)
if err != nil {
warn("could not enhance commit message, %s", err.Error())
return nil
}
if err := appendOnFile(msg, filepath); err != nil {
return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error())
}
return nil
}
}
func readFile(filepath string) (string, error) {
f, err := ioutil.ReadFile(filepath)
if err != nil {
return "", err
}
return string(f), nil
}
func appendOnFile(message, filepath string) error {
f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(message)
return err
}

7
cmd/git-sv/log.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "fmt"
func warn(format string, values ...interface{}) {
fmt.Printf("WARN: "+format+"\n", values...)
}

View File

@ -18,6 +18,7 @@ func main() {
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
outputFormatter := sv.NewOutputFormatter()
validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes)
app := cli.NewApp()
app.Name = "sv"
@ -66,6 +67,17 @@ func main() {
Usage: "generate tag with version based on git commit messages",
Action: tagHandler(git, semverProcessor),
},
{
Name: "validate-commit-message",
Aliases: []string{"vcm"},
Usage: "use as prepare-commit-message hook to validate message",
Action: validateCommitMessageHandler(git, validateMessageProcessor),
Flags: []cli.Flag{
&cli.StringFlag{Name: "path", Required: true, Usage: "git working directory"},
&cli.StringFlag{Name: "file", Required: true, Usage: "name of the file that contains the commit log message"},
&cli.StringFlag{Name: "source", Required: true, Usage: "source of the commit message"},
},
},
}
apperr := app.Run(os.Args)

View File

@ -28,6 +28,7 @@ type Git interface {
Log(initialTag, endTag string) ([]GitCommitLog, error)
Tag(version semver.Version) error
Tags() ([]GitTag, error)
Branch() string
}
// GitCommitLog description of a single commit log
@ -115,6 +116,16 @@ func (g GitImpl) Tags() ([]GitTag, error) {
return parseTagsOutput(string(out))
}
// Branch get git branch
func (GitImpl) Branch() string {
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
out, err := cmd.CombinedOutput()
if err != nil {
return ""
}
return strings.TrimSpace(strings.Trim(string(out), "\n"))
}
func parseTagsOutput(input string) ([]GitTag, error) {
scanner := bufio.NewScanner(strings.NewReader(input))
var result []GitTag

64
sv/validatemessage.go Normal file
View File

@ -0,0 +1,64 @@
package sv
import (
"fmt"
"regexp"
"strings"
)
// ValidateMessageProcessor interface.
type ValidateMessageProcessor interface {
SkipBranch(branch string) bool
Validate(message string) error
Enhance(branch string, message string) (string, error)
}
// NewValidateMessageProcessor ValidateMessageProcessorImpl constructor
func NewValidateMessageProcessor(skipBranches, supportedTypes []string) *ValidateMessageProcessorImpl {
return &ValidateMessageProcessorImpl{
skipBranches: skipBranches,
supportedTypes: supportedTypes,
}
}
// ValidateMessageProcessorImpl process validate message hook.
type ValidateMessageProcessorImpl struct {
skipBranches []string
supportedTypes []string
}
// SkipBranch check if branch should be ignored.
func (p ValidateMessageProcessorImpl) SkipBranch(branch string) bool {
return contains(branch, p.skipBranches)
}
// Validate commit message.
func (p ValidateMessageProcessorImpl) Validate(message string) error {
valid, err := regexp.MatchString("^("+strings.Join(p.supportedTypes, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
if err != nil {
return err
}
if !valid {
return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.supportedTypes)
}
return nil
}
// Enhance add metadata on commit message.
func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (string, error) {
//TODO add issue id (branch format on varenv)
return "", nil
}
func contains(value string, content []string) bool {
for _, v := range content {
if value == v {
return true
}
}
return false
}
func firstLine(value string) string {
return strings.Split(value, "\n")[0]
}

View File

@ -0,0 +1,60 @@
package sv
import (
"testing"
)
func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"})
tests := []struct {
name string
message string
wantErr bool
}{
{"single line valid message", "feat: add something", false},
{"single line valid message with scope", "feat(scope): add something", false},
{"single line invalid type message", "something: add something", true},
{"single line invalid type message", "feat?: add something", true},
{"multi line valid message", `feat: add something
team: x`, false},
{"multi line invalid message", `feat add something
team: x`, true},
{"support ! for breaking change", "feat!: add something", false},
{"support ! with scope for breaking change", "feat(scope)!: add something", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := p.Validate(tt.message); (err != nil) != tt.wantErr {
t.Errorf("ValidateMessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_firstLine(t *testing.T) {
tests := []struct {
name string
value string
want string
}{
{"empty string", "", ""},
{"single line string", "single line", "single line"},
{"multi line string", `first line
last line`, "first line"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := firstLine(tt.value); got != tt.want {
t.Errorf("firstLine() = %v, want %v", got, tt.want)
}
})
}
}