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:
parent
943487fae8
commit
67cd90c762
|
@ -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 {
|
||||
|
|
|
@ -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
7
cmd/git-sv/log.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func warn(format string, values ...interface{}) {
|
||||
fmt.Printf("WARN: "+format+"\n", values...)
|
||||
}
|
|
@ -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)
|
||||
|
|
11
sv/git.go
11
sv/git.go
|
@ -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
64
sv/validatemessage.go
Normal 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]
|
||||
}
|
60
sv/validatemessage_test.go
Normal file
60
sv/validatemessage_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user