From 3843c1abbe6c1b933b4542a2056dfb7c34908c18 Mon Sep 17 00:00:00 2001 From: Christoph Witzko Date: Fri, 16 Feb 2024 15:26:05 +0100 Subject: [PATCH] feat: split-up logic in multiple files --- pkg/analyzer/commit.go | 25 +++++++++ pkg/analyzer/commit_analyzer.go | 46 +++++----------- pkg/analyzer/commit_analyzer_test.go | 68 +---------------------- pkg/analyzer/commit_test.go | 82 ++++++++++++++++++++++++++++ pkg/analyzer/patterns.go | 28 ++++++++++ pkg/analyzer/patterns_test.go | 51 +++++++++++++++++ pkg/analyzer/rules.go | 77 ++++++++++++++++++++++++++ pkg/analyzer/rules_test.go | 62 +++++++++++++++++++++ 8 files changed, 340 insertions(+), 99 deletions(-) create mode 100644 pkg/analyzer/commit.go create mode 100644 pkg/analyzer/commit_test.go create mode 100644 pkg/analyzer/patterns.go create mode 100644 pkg/analyzer/patterns_test.go create mode 100644 pkg/analyzer/rules.go create mode 100644 pkg/analyzer/rules_test.go diff --git a/pkg/analyzer/commit.go b/pkg/analyzer/commit.go new file mode 100644 index 0000000..7a070ce --- /dev/null +++ b/pkg/analyzer/commit.go @@ -0,0 +1,25 @@ +package analyzer + +import "strings" + +type parsedCommit struct { + Type string + Scope string + Modifier string + Message string +} + +func parseCommit(msg string) *parsedCommit { + found := commitPattern.FindAllStringSubmatch(msg, -1) + if len(found) < 1 { + // commit message does not match pattern + return nil + } + + return &parsedCommit{ + Type: strings.ToLower(found[0][1]), + Scope: found[0][2], + Modifier: found[0][3], + Message: found[0][4], + } +} diff --git a/pkg/analyzer/commit_analyzer.go b/pkg/analyzer/commit_analyzer.go index 7b17798..400dd53 100644 --- a/pkg/analyzer/commit_analyzer.go +++ b/pkg/analyzer/commit_analyzer.go @@ -1,33 +1,29 @@ package analyzer import ( - "regexp" "strings" "github.com/go-semantic-release/semantic-release/v2/pkg/semrel" ) -var ( - CAVERSION = "dev" - commitPattern = regexp.MustCompile(`^([^\s\(\!]+)(?:\(([^\)]*)\))?(\!)?\: (.*)$`) - breakingPattern = regexp.MustCompile("BREAKING CHANGES?") - mentionedIssuesPattern = regexp.MustCompile(`#(\d+)`) - mentionedUsersPattern = regexp.MustCompile(`(?i)@([a-z\d]([a-z\d]|-[a-z\d])+)`) -) +var CAVERSION = "dev" -func extractMentions(re *regexp.Regexp, s string) string { - ret := make([]string, 0) - for _, m := range re.FindAllStringSubmatch(s, -1) { - ret = append(ret, m[1]) - } - return strings.Join(ret, ",") +type DefaultCommitAnalyzer struct{} + +func (da *DefaultCommitAnalyzer) Init(m map[string]string) error { + // TODO: implement config parsing + return nil +} + +func (da *DefaultCommitAnalyzer) Name() string { + return "default" } -func matchesBreakingPattern(c *semrel.Commit) bool { - return breakingPattern.MatchString(strings.Join(c.Raw, "\n")) +func (da *DefaultCommitAnalyzer) Version() string { + return CAVERSION } -func setTypeAndChange(c *semrel.Commit) { +func (da *DefaultCommitAnalyzer) setTypeAndChange(c *semrel.Commit) { found := commitPattern.FindAllStringSubmatch(c.Raw[0], -1) if len(found) < 1 { // commit message does not match pattern @@ -46,20 +42,6 @@ func setTypeAndChange(c *semrel.Commit) { } } -type DefaultCommitAnalyzer struct{} - -func (da *DefaultCommitAnalyzer) Init(_ map[string]string) error { - return nil -} - -func (da *DefaultCommitAnalyzer) Name() string { - return "default" -} - -func (da *DefaultCommitAnalyzer) Version() string { - return CAVERSION -} - func (da *DefaultCommitAnalyzer) analyzeSingleCommit(rawCommit *semrel.RawCommit) *semrel.Commit { c := &semrel.Commit{ SHA: rawCommit.SHA, @@ -70,7 +52,7 @@ func (da *DefaultCommitAnalyzer) analyzeSingleCommit(rawCommit *semrel.RawCommit c.Annotations["mentioned_issues"] = extractMentions(mentionedIssuesPattern, rawCommit.RawMessage) c.Annotations["mentioned_users"] = extractMentions(mentionedUsersPattern, rawCommit.RawMessage) - setTypeAndChange(c) + da.setTypeAndChange(c) return c } diff --git a/pkg/analyzer/commit_analyzer_test.go b/pkg/analyzer/commit_analyzer_test.go index d397fee..0173bc3 100644 --- a/pkg/analyzer/commit_analyzer_test.go +++ b/pkg/analyzer/commit_analyzer_test.go @@ -1,7 +1,6 @@ package analyzer import ( - "fmt" "strings" "testing" @@ -101,7 +100,7 @@ func TestDefaultAnalyzer(t *testing.T) { defaultAnalyzer := &DefaultCommitAnalyzer{} for _, tc := range testCases { - t.Run(fmt.Sprintf("AnalyzeCommitMessage: %s", tc.RawCommit.RawMessage), func(t *testing.T) { + t.Run(tc.RawCommit.RawMessage, func(t *testing.T) { analyzedCommit := defaultAnalyzer.analyzeSingleCommit(tc.RawCommit) require.Equal(t, tc.Type, analyzedCommit.Type, "Type") require.Equal(t, tc.Scope, analyzedCommit.Scope, "Scope") @@ -111,68 +110,3 @@ func TestDefaultAnalyzer(t *testing.T) { }) } } - -func TestCommitPattern(t *testing.T) { - testCases := []struct { - message string - wanted []string - }{ - { - message: "feat: new feature", - wanted: []string{"feat", "", "", "new feature"}, - }, - { - message: "feat!: new feature", - wanted: []string{"feat", "", "!", "new feature"}, - }, - { - message: "feat(api): new feature", - wanted: []string{"feat", "api", "", "new feature"}, - }, - { - message: "feat(api): a(b): c:", - wanted: []string{"feat", "api", "", "a(b): c:"}, - }, - { - message: "feat(new cool-api): feature", - wanted: []string{"feat", "new cool-api", "", "feature"}, - }, - { - message: "feat(😅): cool", - wanted: []string{"feat", "😅", "", "cool"}, - }, - { - message: "this-is-also(valid): cool", - wanted: []string{"this-is-also", "valid", "", "cool"}, - }, - { - message: "🚀(🦄): emojis!", - wanted: []string{"🚀", "🦄", "", "emojis!"}, - }, - // invalid messages - { - message: "feat (new api): feature", - wanted: nil, - }, - { - message: "feat((x)): test", - wanted: nil, - }, - { - message: "feat:test", - wanted: nil, - }, - } - for _, tc := range testCases { - t.Run(fmt.Sprintf("CommitPattern: %s", tc.message), func(t *testing.T) { - found := commitPattern.FindAllStringSubmatch(tc.message, -1) - if len(tc.wanted) == 0 { - require.Len(t, found, 0) - return - } - require.Len(t, found, 1) - require.Len(t, found[0], 5) - require.Equal(t, tc.wanted, found[0][1:]) - }) - } -} diff --git a/pkg/analyzer/commit_test.go b/pkg/analyzer/commit_test.go new file mode 100644 index 0000000..bb9814d --- /dev/null +++ b/pkg/analyzer/commit_test.go @@ -0,0 +1,82 @@ +package analyzer + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseCommit(t *testing.T) { + testCases := []struct { + message string + wanted *parsedCommit + }{ + { + message: "feat: new feature", + wanted: &parsedCommit{"feat", "", "", "new feature"}, + }, + { + message: "feat!: new feature", + wanted: &parsedCommit{"feat", "", "!", "new feature"}, + }, + { + message: "feat(api): new feature", + wanted: &parsedCommit{"feat", "api", "", "new feature"}, + }, + { + message: "feat(api): a(b): c:", + wanted: &parsedCommit{"feat", "api", "", "a(b): c:"}, + }, + { + message: "feat(new cool-api): feature", + wanted: &parsedCommit{"feat", "new cool-api", "", "feature"}, + }, + { + message: "feat(😅): cool", + wanted: &parsedCommit{"feat", "😅", "", "cool"}, + }, + { + message: "this-is-also(valid): cool", + wanted: &parsedCommit{"this-is-also", "valid", "", "cool"}, + }, + { + message: "feat((x)): test", + wanted: &parsedCommit{"feat", "(x", ")", "test"}, + }, + { + message: "feat(x)?!: test", + wanted: &parsedCommit{"feat", "x", "?!", "test"}, + }, + { + message: "feat(x): test", + wanted: &parsedCommit{"feat", "x", "", "test"}, + }, + { + message: "feat(x): : test", + wanted: &parsedCommit{"feat", "x", "", ": test"}, + }, + { + message: "feat!: test", + wanted: &parsedCommit{"feat", "", "!", "test"}, + }, + // invalid messages + { + message: "feat (new api): feature", + wanted: nil, + }, + { + message: "feat:test", + wanted: nil, + }, + { + message: "🚀(🦄): emojis!", + wanted: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.message, func(t *testing.T) { + c := parseCommit(tc.message) + require.Equal(t, tc.wanted, c) + }) + } +} diff --git a/pkg/analyzer/patterns.go b/pkg/analyzer/patterns.go new file mode 100644 index 0000000..60174f2 --- /dev/null +++ b/pkg/analyzer/patterns.go @@ -0,0 +1,28 @@ +package analyzer + +import ( + "regexp" + "strings" + + "github.com/go-semantic-release/semantic-release/v2/pkg/semrel" +) + +var ( + releaseRulePattern = regexp.MustCompile(`^([\w-\*]+)(?:\(([^\)]*)\))?(\S*)$`) + commitPattern = regexp.MustCompile(`^([\w-]+)(?:\(([^\)]*)\))?(\S*)\: (.*)$`) + breakingPattern = regexp.MustCompile("BREAKING CHANGES?") + mentionedIssuesPattern = regexp.MustCompile(`#(\d+)`) + mentionedUsersPattern = regexp.MustCompile(`(?i)@([a-z\d]([a-z\d]|-[a-z\d])+)`) +) + +func extractMentions(re *regexp.Regexp, s string) string { + ret := make([]string, 0) + for _, m := range re.FindAllStringSubmatch(s, -1) { + ret = append(ret, m[1]) + } + return strings.Join(ret, ",") +} + +func matchesBreakingPattern(c *semrel.Commit) bool { + return breakingPattern.MatchString(strings.Join(c.Raw, "\n")) +} diff --git a/pkg/analyzer/patterns_test.go b/pkg/analyzer/patterns_test.go new file mode 100644 index 0000000..bcfe23c --- /dev/null +++ b/pkg/analyzer/patterns_test.go @@ -0,0 +1,51 @@ +package analyzer + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExtractIssues(t *testing.T) { + testCases := []struct { + message string + wanted string + }{ + { + message: "feat: new feature #123", + wanted: "123", + }, + { + message: "feat!: new feature closes #123 and #456", + wanted: "123,456", + }, + } + for _, testCase := range testCases { + t.Run(testCase.message, func(t *testing.T) { + issues := extractMentions(mentionedIssuesPattern, testCase.message) + require.Equal(t, testCase.wanted, issues) + }) + } +} + +func TestExtractMentions(t *testing.T) { + testCases := []struct { + message string + wanted string + }{ + { + message: "feat: new feature by @user", + wanted: "user", + }, + { + message: "feat!: new feature by @user and @user-2", + wanted: "user,user-2", + }, + } + for _, testCase := range testCases { + t.Run(testCase.message, func(t *testing.T) { + issues := extractMentions(mentionedUsersPattern, testCase.message) + require.Equal(t, testCase.wanted, issues) + }) + } +} diff --git a/pkg/analyzer/rules.go b/pkg/analyzer/rules.go new file mode 100644 index 0000000..bb2ef7a --- /dev/null +++ b/pkg/analyzer/rules.go @@ -0,0 +1,77 @@ +package analyzer + +import ( + "cmp" + "fmt" + "strings" +) + +var ( + defaultMajorReleaseRules = "*(*)!" + defaultMinorReleaseRules = "feat" + defaultPatchReleaseRules = "fix" +) + +type releaseRule struct { + Type string + Scope string + Modifier string +} + +func (r *releaseRule) String() string { + return fmt.Sprintf("%s(%s)%s", r.Type, r.Scope, r.Modifier) +} + +func (r *releaseRule) Matches(commit *parsedCommit) bool { + return (r.Type == "*" || r.Type == commit.Type) && + (r.Scope == "*" || r.Scope == commit.Scope) && + (r.Modifier == "*" || r.Modifier == commit.Modifier) +} + +func parseRule(rule string) (*releaseRule, error) { + foundRule := releaseRulePattern.FindAllStringSubmatch(rule, -1) + if len(foundRule) < 1 { + return nil, fmt.Errorf("cannot parse rule: %s", rule) + } + return &releaseRule{ + Type: strings.ToLower(foundRule[0][1]), + // undefined scope defaults to * + Scope: cmp.Or(foundRule[0][2], "*"), + Modifier: foundRule[0][3], + }, nil +} + +type releaseRules []*releaseRule + +func (r releaseRules) String() string { + ret := make([]string, len(r)) + for i, rule := range r { + ret[i] = rule.String() + } + return strings.Join(ret, ",") +} + +func (r releaseRules) Matches(commit *parsedCommit) bool { + for _, rule := range r { + if rule.Matches(commit) { + return true + } + } + return false +} + +func parseRules(rules string) (releaseRules, error) { + if rules == "" { + return nil, fmt.Errorf("no rules provided") + } + ruleStrings := strings.Split(rules, ",") + ret := make(releaseRules, len(ruleStrings)) + for i, r := range ruleStrings { + parsed, err := parseRule(r) + if err != nil { + return nil, err + } + ret[i] = parsed + } + return ret, nil +} diff --git a/pkg/analyzer/rules_test.go b/pkg/analyzer/rules_test.go new file mode 100644 index 0000000..a6ab98e --- /dev/null +++ b/pkg/analyzer/rules_test.go @@ -0,0 +1,62 @@ +package analyzer + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseRule(t *testing.T) { + testCases := []struct { + rule string + wanted *releaseRule + }{ + { + rule: "feat", + wanted: &releaseRule{Type: "feat", Scope: "*", Modifier: ""}, + }, + { + rule: "feat(api)", + wanted: &releaseRule{Type: "feat", Scope: "api", Modifier: ""}, + }, + { + rule: "feat(*)!", + wanted: &releaseRule{Type: "feat", Scope: "*", Modifier: "!"}, + }, + { + rule: "feat(api)!", + wanted: &releaseRule{Type: "feat", Scope: "api", Modifier: "!"}, + }, + { + rule: "*(*)!", + wanted: &releaseRule{Type: "*", Scope: "*", Modifier: "!"}, + }, + { + rule: "*(*)*", + wanted: &releaseRule{Type: "*", Scope: "*", Modifier: "*"}, + }, + { + rule: "*", + wanted: &releaseRule{Type: "*", Scope: "*", Modifier: ""}, + }, + { + rule: "*!", + wanted: &releaseRule{Type: "*", Scope: "*", Modifier: "!"}, + }, + { + rule: "x!", + wanted: &releaseRule{Type: "x", Scope: "*", Modifier: "!"}, + }, + { + rule: "x🦄", + wanted: &releaseRule{Type: "x", Scope: "*", Modifier: "🦄"}, + }, + } + for _, tc := range testCases { + t.Run(tc.rule, func(t *testing.T) { + r, err := parseRule(tc.rule) + require.NoError(t, err) + require.Equal(t, tc.wanted, r) + }) + } +}