feat: parse conventional commits
All checks were successful
build / build (push) Successful in 1m14s
build / trigger-build-image (push) Successful in 6s

This commit is contained in:
张泊明518370910136 2024-10-02 05:50:40 -04:00
parent 7cc6747a25
commit d723d98de0
GPG Key ID: D47306D7062CDA9D
3 changed files with 144 additions and 67 deletions

View File

@ -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
View 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)
}
})
}
}

View File

@ -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