From d723d98de0228ba9a2af97426f502600cf662f85 Mon Sep 17 00:00:00 2001 From: Boming Zhang Date: Wed, 2 Oct 2024 05:50:40 -0400 Subject: [PATCH] feat: parse conventional commits --- cmd/joj3/conf.go | 100 ++++++++++++++++++------------------------ cmd/joj3/conf_test.go | 86 ++++++++++++++++++++++++++++++++++++ cmd/joj3/main.go | 25 ++++++----- 3 files changed, 144 insertions(+), 67 deletions(-) create mode 100644 cmd/joj3/conf_test.go diff --git a/cmd/joj3/conf.go b/cmd/joj3/conf.go index 95c9d83..6f615df 100644 --- a/cmd/joj3/conf.go +++ b/cmd/joj3/conf.go @@ -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 } diff --git a/cmd/joj3/conf_test.go b/cmd/joj3/conf_test.go new file mode 100644 index 0000000..5d01d6e --- /dev/null +++ b/cmd/joj3/conf_test.go @@ -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) + } + }) + } +} diff --git a/cmd/joj3/main.go b/cmd/joj3/main.go index c73641c..7539ba0 100644 --- a/cmd/joj3/main.go +++ b/cmd/joj3/main.go @@ -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 - msg string - showVersion *bool - Version string + confRoot string + confName string + msg string + showVersion *bool + 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