feat: parse conventional commits
This commit is contained in:
parent
7cc6747a25
commit
d723d98de0
100
cmd/joj3/conf.go
100
cmd/joj3/conf.go
|
@ -1,10 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -19,6 +18,7 @@ type Conf struct {
|
||||||
OutputPath string `default:"joj3_result.json"`
|
OutputPath string `default:"joj3_result.json"`
|
||||||
Stages []struct {
|
Stages []struct {
|
||||||
Name string
|
Name string
|
||||||
|
Group string
|
||||||
Executor struct {
|
Executor struct {
|
||||||
Name string
|
Name string
|
||||||
With struct {
|
With struct {
|
||||||
|
@ -64,54 +64,28 @@ type OptionalCmd struct {
|
||||||
AddressSpaceLimit *bool
|
AddressSpaceLimit *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add other fields to match? not only limit to latest commit message
|
type ConventionalCommit struct {
|
||||||
type MetaConf struct {
|
Type string
|
||||||
Patterns []struct {
|
Scope string
|
||||||
Filename string
|
Description string
|
||||||
Regex string
|
Body string
|
||||||
}
|
Footer string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMetaConfFile(path string) (metaConf MetaConf, err error) {
|
func parseConventionalCommit(commit string) (*ConventionalCommit, error) {
|
||||||
// FIXME: remove this default meta config, it is only for demonstration
|
re := regexp.MustCompile(`^(\w+)(\(([^)]+)\))?!?: (.+)(\n\n(.+))?(\n\n(.+))?$`)
|
||||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
matches := re.FindStringSubmatch(strings.TrimSpace(commit))
|
||||||
slog.Debug("meta conf not found", "path", path)
|
if matches == nil {
|
||||||
return MetaConf{
|
return nil, fmt.Errorf("invalid conventional commit format")
|
||||||
Patterns: []struct {
|
|
||||||
Filename string
|
|
||||||
Regex string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Filename: "conf.json",
|
|
||||||
Regex: ".*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
d := &multiconfig.DefaultLoader{}
|
cc := &ConventionalCommit{
|
||||||
loaders := []multiconfig.Loader{}
|
Type: matches[1],
|
||||||
loaders = append(loaders, &multiconfig.TagLoader{})
|
Scope: matches[3],
|
||||||
if strings.HasSuffix(path, "toml") {
|
Description: strings.TrimSpace(matches[4]),
|
||||||
loaders = append(loaders, &multiconfig.TOMLLoader{Path: path})
|
Body: strings.TrimSpace(matches[6]),
|
||||||
|
Footer: strings.TrimSpace(matches[8]),
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(path, "json") {
|
return cc, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseConfFile(path string) (conf Conf, err error) {
|
func parseConfFile(path string) (conf Conf, err error) {
|
||||||
|
@ -132,20 +106,32 @@ func parseConfFile(path string) (conf Conf, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func msgToConf(metaConfPath string, msg string) (conf Conf, err error) {
|
func parseMsg(confRoot, confName, msg string) (conf Conf, group string, err error) {
|
||||||
slog.Info("msg to conf", "msg", msg)
|
slog.Info("parse msg", "msg", msg)
|
||||||
metaConf, err := parseMetaConfFile(metaConfPath)
|
conventionalCommit, err := parseConventionalCommit(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, pattern := range metaConf.Patterns {
|
slog.Info("conventional commit", "commit", conventionalCommit)
|
||||||
if matched, _ := regexp.MatchString(pattern.Regex, msg); matched {
|
confRoot = filepath.Clean(confRoot)
|
||||||
slog.Debug("pattern matched",
|
confPath := filepath.Clean(fmt.Sprintf("%s/%s/%s",
|
||||||
"pattern", pattern, "filename", pattern.Filename)
|
confRoot, conventionalCommit.Scope, confName))
|
||||||
slog.Info("pattern matched", "filename", pattern.Filename)
|
relPath, err := filepath.Rel(confRoot, confPath)
|
||||||
return parseConfFile(pattern.Filename)
|
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
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateStages(conf Conf) ([]stage.Stage, error) {
|
func generateStages(conf Conf, group string) ([]stage.Stage, error) {
|
||||||
stages := []stage.Stage{}
|
stages := []stage.Stage{}
|
||||||
for _, s := range conf.Stages {
|
for _, s := range conf.Stages {
|
||||||
|
if s.Group != "" && group != "" && group != s.Group {
|
||||||
|
continue
|
||||||
|
}
|
||||||
var cmds []stage.Cmd
|
var cmds []stage.Cmd
|
||||||
defaultCmd := s.Executor.With.Default
|
defaultCmd := s.Executor.With.Default
|
||||||
for _, optionalCmd := range s.Executor.With.Cases {
|
for _, optionalCmd := range s.Executor.With.Cases {
|
||||||
|
@ -81,14 +84,16 @@ func outputResult(outputPath string, results []stage.StageResult) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metaConfPath string
|
confRoot string
|
||||||
msg string
|
confName string
|
||||||
showVersion *bool
|
msg string
|
||||||
Version string
|
showVersion *bool
|
||||||
|
Version string = "debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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")
|
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")
|
showVersion = flag.Bool("version", false, "print current version")
|
||||||
}
|
}
|
||||||
|
@ -97,12 +102,12 @@ func mainImpl() error {
|
||||||
if err := setupSlog(""); err != nil { // before conf is loaded
|
if err := setupSlog(""); err != nil { // before conf is loaded
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
slog.Info("start joj3", "version", Version)
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Println(Version)
|
fmt.Println(Version)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
slog.Info("start joj3", "version", Version)
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
var err error
|
var err error
|
||||||
msg, err = getCommitMsg()
|
msg, err = getCommitMsg()
|
||||||
|
@ -111,9 +116,9 @@ func mainImpl() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conf, err := msgToConf(metaConfPath, msg)
|
conf, group, err := parseMsg(confRoot, confName, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("no conf found", "error", err)
|
slog.Error("parse msg", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := setupSlog(conf.LogPath); err != nil { // after conf is loaded
|
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.Info("debug log", "path", conf.LogPath)
|
||||||
slog.Debug("conf loaded", "conf", conf)
|
slog.Debug("conf loaded", "conf", conf)
|
||||||
executors.InitWithConf(conf.SandboxExecServer, conf.SandboxToken)
|
executors.InitWithConf(conf.SandboxExecServer, conf.SandboxToken)
|
||||||
stages, err := generateStages(conf)
|
stages, err := generateStages(conf, group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("generate stages", "error", err)
|
slog.Error("generate stages", "error", err)
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in New Issue
Block a user