feat: parse conventional commits
This commit is contained in:
parent
7cc6747a25
commit
d723d98de0
|
@ -1,10 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
|
@ -19,6 +18,7 @@ type Conf struct {
|
|||
OutputPath string `default:"joj3_result.json"`
|
||||
Stages []struct {
|
||||
Name string
|
||||
Group string
|
||||
Executor struct {
|
||||
Name string
|
||||
With struct {
|
||||
|
@ -64,54 +64,28 @@ type OptionalCmd struct {
|
|||
AddressSpaceLimit *bool
|
||||
}
|
||||
|
||||
// TODO: add other fields to match? not only limit to latest commit message
|
||||
type MetaConf struct {
|
||||
Patterns []struct {
|
||||
Filename string
|
||||
Regex string
|
||||
}
|
||||
type ConventionalCommit struct {
|
||||
Type string
|
||||
Scope string
|
||||
Description string
|
||||
Body string
|
||||
Footer string
|
||||
}
|
||||
|
||||
func parseMetaConfFile(path string) (metaConf MetaConf, err error) {
|
||||
// FIXME: remove this default meta config, it is only for demonstration
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
slog.Debug("meta conf not found", "path", path)
|
||||
return MetaConf{
|
||||
Patterns: []struct {
|
||||
Filename string
|
||||
Regex string
|
||||
}{
|
||||
{
|
||||
Filename: "conf.json",
|
||||
Regex: ".*",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
func parseConventionalCommit(commit string) (*ConventionalCommit, error) {
|
||||
re := regexp.MustCompile(`^(\w+)(\(([^)]+)\))?!?: (.+)(\n\n(.+))?(\n\n(.+))?$`)
|
||||
matches := re.FindStringSubmatch(strings.TrimSpace(commit))
|
||||
if matches == nil {
|
||||
return nil, fmt.Errorf("invalid conventional commit format")
|
||||
}
|
||||
d := &multiconfig.DefaultLoader{}
|
||||
loaders := []multiconfig.Loader{}
|
||||
loaders = append(loaders, &multiconfig.TagLoader{})
|
||||
if strings.HasSuffix(path, "toml") {
|
||||
loaders = append(loaders, &multiconfig.TOMLLoader{Path: path})
|
||||
cc := &ConventionalCommit{
|
||||
Type: matches[1],
|
||||
Scope: matches[3],
|
||||
Description: strings.TrimSpace(matches[4]),
|
||||
Body: strings.TrimSpace(matches[6]),
|
||||
Footer: strings.TrimSpace(matches[8]),
|
||||
}
|
||||
if strings.HasSuffix(path, "json") {
|
||||
loaders = append(loaders, &multiconfig.JSONLoader{Path: path})
|
||||
}
|
||||
if strings.HasSuffix(path, "yml") || strings.HasSuffix(path, "yaml") {
|
||||
loaders = append(loaders, &multiconfig.YAMLLoader{Path: path})
|
||||
}
|
||||
d.Loader = multiconfig.MultiLoader(loaders...)
|
||||
d.Validator = multiconfig.MultiValidator(&multiconfig.RequiredValidator{})
|
||||
if err = d.Load(&metaConf); err != nil {
|
||||
slog.Error("parse meta conf", "error", err)
|
||||
return
|
||||
}
|
||||
if err = d.Validate(&metaConf); err != nil {
|
||||
slog.Error("validate meta conf", "error", err)
|
||||
return
|
||||
}
|
||||
slog.Debug("meta conf loaded", "metaConf", metaConf)
|
||||
return
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func parseConfFile(path string) (conf Conf, err error) {
|
||||
|
@ -132,20 +106,32 @@ func parseConfFile(path string) (conf Conf, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func msgToConf(metaConfPath string, msg string) (conf Conf, err error) {
|
||||
slog.Info("msg to conf", "msg", msg)
|
||||
metaConf, err := parseMetaConfFile(metaConfPath)
|
||||
func parseMsg(confRoot, confName, msg string) (conf Conf, group string, err error) {
|
||||
slog.Info("parse msg", "msg", msg)
|
||||
conventionalCommit, err := parseConventionalCommit(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, pattern := range metaConf.Patterns {
|
||||
if matched, _ := regexp.MatchString(pattern.Regex, msg); matched {
|
||||
slog.Debug("pattern matched",
|
||||
"pattern", pattern, "filename", pattern.Filename)
|
||||
slog.Info("pattern matched", "filename", pattern.Filename)
|
||||
return parseConfFile(pattern.Filename)
|
||||
slog.Info("conventional commit", "commit", conventionalCommit)
|
||||
confRoot = filepath.Clean(confRoot)
|
||||
confPath := filepath.Clean(fmt.Sprintf("%s/%s/%s",
|
||||
confRoot, conventionalCommit.Scope, confName))
|
||||
relPath, err := filepath.Rel(confRoot, confPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(relPath, "..") {
|
||||
err = fmt.Errorf("invalid scope as path: %s", conventionalCommit.Scope)
|
||||
return
|
||||
}
|
||||
slog.Info("try to load conf", "path", confPath)
|
||||
conf, err = parseConfFile(confPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.Contains(
|
||||
strings.ToLower(conventionalCommit.Description), "joj") {
|
||||
group = "joj"
|
||||
}
|
||||
err = fmt.Errorf("no pattern matched")
|
||||
return
|
||||
}
|
||||
|
|
86
cmd/joj3/conf_test.go
Normal file
86
cmd/joj3/conf_test.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseConventionalCommit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commit string
|
||||
want *ConventionalCommit
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Simple feat commit",
|
||||
commit: "feat: add new feature",
|
||||
want: &ConventionalCommit{
|
||||
Type: "feat",
|
||||
Description: "add new feature",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Commit with scope",
|
||||
commit: "fix(core): resolve memory leak",
|
||||
want: &ConventionalCommit{
|
||||
Type: "fix",
|
||||
Scope: "core",
|
||||
Description: "resolve memory leak",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Breaking change commit",
|
||||
commit: "feat(api)!: redesign user authentication",
|
||||
want: &ConventionalCommit{
|
||||
Type: "feat",
|
||||
Scope: "api",
|
||||
Description: "redesign user authentication",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Commit with body",
|
||||
commit: "docs: update README\n\nAdd installation instructions and improve examples",
|
||||
want: &ConventionalCommit{
|
||||
Type: "docs",
|
||||
Description: "update README",
|
||||
Body: "Add installation instructions and improve examples",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Full commit with body and footer",
|
||||
commit: "feat(auth)!: implement OAuth2\n\nThis commit adds OAuth2 support to the authentication system.\n\nBREAKING CHANGE: Previous authentication tokens are no longer valid.",
|
||||
want: &ConventionalCommit{
|
||||
Type: "feat",
|
||||
Scope: "auth",
|
||||
Description: "implement OAuth2",
|
||||
Body: "This commit adds OAuth2 support to the authentication system.",
|
||||
Footer: "BREAKING CHANGE: Previous authentication tokens are no longer valid.",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid commit format",
|
||||
commit: "This is not a valid conventional commit",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseConventionalCommit(tt.commit)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseConventionalCommit() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseConventionalCommit() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -32,9 +32,12 @@ func getCommitMsg() (msg string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func generateStages(conf Conf) ([]stage.Stage, error) {
|
||||
func generateStages(conf Conf, group string) ([]stage.Stage, error) {
|
||||
stages := []stage.Stage{}
|
||||
for _, s := range conf.Stages {
|
||||
if s.Group != "" && group != "" && group != s.Group {
|
||||
continue
|
||||
}
|
||||
var cmds []stage.Cmd
|
||||
defaultCmd := s.Executor.With.Default
|
||||
for _, optionalCmd := range s.Executor.With.Cases {
|
||||
|
@ -81,14 +84,16 @@ func outputResult(outputPath string, results []stage.StageResult) error {
|
|||
}
|
||||
|
||||
var (
|
||||
metaConfPath string
|
||||
confRoot string
|
||||
confName string
|
||||
msg string
|
||||
showVersion *bool
|
||||
Version string
|
||||
Version string = "debug"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&metaConfPath, "meta-conf", "meta-conf.toml", "meta config file path")
|
||||
flag.StringVar(&confRoot, "conf-root", ".", "root path for all config files")
|
||||
flag.StringVar(&confName, "conf-name", "conf.json", "filename for config files")
|
||||
flag.StringVar(&msg, "msg", "", "message to trigger the running, leave empty to use git commit message on HEAD")
|
||||
showVersion = flag.Bool("version", false, "print current version")
|
||||
}
|
||||
|
@ -97,12 +102,12 @@ func mainImpl() error {
|
|||
if err := setupSlog(""); err != nil { // before conf is loaded
|
||||
return err
|
||||
}
|
||||
slog.Info("start joj3", "version", Version)
|
||||
flag.Parse()
|
||||
if *showVersion {
|
||||
fmt.Println(Version)
|
||||
return nil
|
||||
}
|
||||
slog.Info("start joj3", "version", Version)
|
||||
if msg == "" {
|
||||
var err error
|
||||
msg, err = getCommitMsg()
|
||||
|
@ -111,9 +116,9 @@ func mainImpl() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
conf, err := msgToConf(metaConfPath, msg)
|
||||
conf, group, err := parseMsg(confRoot, confName, msg)
|
||||
if err != nil {
|
||||
slog.Error("no conf found", "error", err)
|
||||
slog.Error("parse msg", "error", err)
|
||||
return err
|
||||
}
|
||||
if err := setupSlog(conf.LogPath); err != nil { // after conf is loaded
|
||||
|
@ -122,7 +127,7 @@ func mainImpl() error {
|
|||
slog.Info("debug log", "path", conf.LogPath)
|
||||
slog.Debug("conf loaded", "conf", conf)
|
||||
executors.InitWithConf(conf.SandboxExecServer, conf.SandboxToken)
|
||||
stages, err := generateStages(conf)
|
||||
stages, err := generateStages(conf, group)
|
||||
if err != nil {
|
||||
slog.Error("generate stages", "error", err)
|
||||
return err
|
||||
|
|
Loading…
Reference in New Issue
Block a user