You've already forked go-semantic-release
feat(analyzer): add conventional commit format
See: [conventional](https://www.conventionalcommits.org/en/v1.0.0/#summaryhttps://www.conventionalcommits.org/en/v1.0.0/#summary)
This commit is contained in:
@@ -38,6 +38,9 @@ func New(format string, config config.ChangelogConfig) (*Analyzer, error) {
|
||||
case ANGULAR:
|
||||
analyzer.analyzeCommits = newAngular()
|
||||
log.Debugf("Commit format set to %s", ANGULAR)
|
||||
case CONVENTIONAL:
|
||||
analyzer.analyzeCommits = newConventional()
|
||||
log.Debugf("Commit format set to %s", CONVENTIONAL)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid commit format: %s", format)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type angular struct {
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
// ANGULAR identifer
|
||||
// ANGULAR identifier
|
||||
const ANGULAR = "angular"
|
||||
|
||||
func newAngular() *angular {
|
||||
|
||||
@@ -19,8 +19,8 @@ func TestAngular(t *testing.T) {
|
||||
{
|
||||
testCase: "feat",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
@@ -33,12 +33,12 @@ func TestAngular(t *testing.T) {
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"major": []shared.AnalyzedCommit{},
|
||||
"patch": []shared.AnalyzedCommit{},
|
||||
"none": []shared.AnalyzedCommit{},
|
||||
"major": {},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
@@ -48,8 +48,8 @@ func TestAngular(t *testing.T) {
|
||||
{
|
||||
testCase: "feat breaking change",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
@@ -62,8 +62,8 @@ func TestAngular(t *testing.T) {
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"major": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"major": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first break BREAKING CHANGE: change api to v2",
|
||||
Author: "me",
|
||||
@@ -77,38 +77,38 @@ func TestAngular(t *testing.T) {
|
||||
ParsedBreakingChangeMessage: "change api to v2",
|
||||
},
|
||||
},
|
||||
"patch": []shared.AnalyzedCommit{},
|
||||
"none": []shared.AnalyzedCommit{},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "feat(internal/changelog): my first break BREAKING CHANGE: change api to v2",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ testCase: "feat breaking change footer",
|
||||
{testCase: "feat breaking change footer",
|
||||
commits: []shared.Commit{
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
shared.Commit{
|
||||
Message: "feat(internal/changelog): my first break \n\nBREAKING CHANGE: change api to v2\n",
|
||||
},
|
||||
{
|
||||
Message: "feat(internal/changelog): my first break \n\nBREAKING CHANGE: change api to v2\n",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
},
|
||||
},
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
@@ -121,8 +121,8 @@ func TestAngular(t *testing.T) {
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"major": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"major": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first break \n\nBREAKING CHANGE: change api to v2\n",
|
||||
Author: "me",
|
||||
@@ -136,25 +136,25 @@ func TestAngular(t *testing.T) {
|
||||
ParsedBreakingChangeMessage: "change api to v2",
|
||||
},
|
||||
},
|
||||
"patch": []shared.AnalyzedCommit{},
|
||||
"none": []shared.AnalyzedCommit{},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
testCase: "invalid",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": []shared.AnalyzedCommit{},
|
||||
"major": []shared.AnalyzedCommit{},
|
||||
"patch": []shared.AnalyzedCommit{},
|
||||
"none": []shared.AnalyzedCommit{},
|
||||
"minor": {},
|
||||
"major": {},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "internal/changelog: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "Merge feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
@@ -164,8 +164,8 @@ func TestAngular(t *testing.T) {
|
||||
{
|
||||
testCase: "feat and build",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
@@ -178,8 +178,8 @@ func TestAngular(t *testing.T) {
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"none": []shared.AnalyzedCommit{
|
||||
shared.AnalyzedCommit{
|
||||
"none": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "build(internal/changelog): my first build",
|
||||
Author: "me",
|
||||
@@ -193,16 +193,16 @@ func TestAngular(t *testing.T) {
|
||||
ParsedBreakingChangeMessage: "",
|
||||
},
|
||||
},
|
||||
"patch": []shared.AnalyzedCommit{},
|
||||
"major": []shared.AnalyzedCommit{},
|
||||
"patch": {},
|
||||
"major": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
shared.Commit{
|
||||
{
|
||||
Message: "build(internal/changelog): my first build",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
|
||||
130
internal/analyzer/conventional.go
Normal file
130
internal/analyzer/conventional.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Package analyzer provides different commit analyzer
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Nightapes/go-semantic-release/internal/shared"
|
||||
)
|
||||
|
||||
type conventional struct {
|
||||
rules []Rule
|
||||
regex string
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
// CONVENTIONAL identifier
|
||||
const CONVENTIONAL = "conventional"
|
||||
|
||||
func newConventional() *conventional {
|
||||
return &conventional{
|
||||
regex: `^(TAG)(?:\((.*)\))?(\!)?: (?s)(.*)`,
|
||||
log: log.WithField("analyzer", CONVENTIONAL),
|
||||
rules: []Rule{
|
||||
{
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Release: "minor",
|
||||
Changelog: true,
|
||||
},
|
||||
{
|
||||
Tag: "fix",
|
||||
TagString: "Bug fixes",
|
||||
Release: "patch",
|
||||
Changelog: true,
|
||||
}, {
|
||||
Tag: "perf",
|
||||
TagString: "Performance improvments",
|
||||
Release: "patch",
|
||||
Changelog: true,
|
||||
}, {
|
||||
Tag: "docs",
|
||||
TagString: "Documentation changes",
|
||||
Release: "none",
|
||||
Changelog: false,
|
||||
},
|
||||
{
|
||||
Tag: "style",
|
||||
TagString: "Style",
|
||||
Release: "none",
|
||||
Changelog: false,
|
||||
}, {
|
||||
Tag: "refactor",
|
||||
TagString: "Code refactor",
|
||||
Release: "none",
|
||||
Changelog: false,
|
||||
}, {
|
||||
Tag: "test",
|
||||
TagString: "Testing",
|
||||
Release: "none",
|
||||
Changelog: false,
|
||||
}, {
|
||||
Tag: "chore",
|
||||
TagString: "Changes to the build process or auxiliary tools and libraries such as documentation generation",
|
||||
Release: "none",
|
||||
Changelog: false,
|
||||
}, {
|
||||
Tag: "build",
|
||||
TagString: "Changes to CI/CD",
|
||||
Release: "none",
|
||||
Changelog: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *conventional) getRules() []Rule {
|
||||
return a.rules
|
||||
}
|
||||
|
||||
func (a *conventional) analyze(commit shared.Commit, rule Rule) (shared.AnalyzedCommit, bool, error) {
|
||||
|
||||
analyzed := shared.AnalyzedCommit{
|
||||
Commit: commit,
|
||||
Tag: rule.Tag,
|
||||
TagString: rule.TagString,
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(strings.Replace(a.regex, "TAG", rule.Tag, -1))
|
||||
matches := re.FindAllStringSubmatch(commit.Message, -1)
|
||||
if len(matches) >= 1 {
|
||||
if len(matches[0]) >= 4 {
|
||||
|
||||
analyzed.Scope = shared.Scope(matches[0][2])
|
||||
|
||||
message := strings.Join(matches[0][4:], "")
|
||||
if matches[0][3] == "" && !strings.Contains(message, "BREAKING CHANGE:") {
|
||||
analyzed.ParsedMessage = strings.Trim(message, " ")
|
||||
|
||||
a.log.Tracef("%s: found %s", commit.Message, rule.Tag)
|
||||
return analyzed, false, nil
|
||||
}
|
||||
|
||||
if matches[0][3] == "" {
|
||||
breakingChange := strings.SplitN(message, "BREAKING CHANGE:", 2)
|
||||
analyzed.ParsedMessage = strings.TrimSpace(breakingChange[0])
|
||||
analyzed.ParsedBreakingChangeMessage = strings.TrimSpace(breakingChange[1])
|
||||
a.log.Tracef(" %s, BREAKING CHANGE found", commit.Message)
|
||||
return analyzed, true, nil
|
||||
}
|
||||
|
||||
breakingChange := strings.SplitN(message, "BREAKING CHANGE:", 2)
|
||||
|
||||
if len(breakingChange) > 1 {
|
||||
analyzed.ParsedMessage = strings.TrimSpace(breakingChange[0])
|
||||
analyzed.ParsedBreakingChangeMessage = strings.TrimSpace(breakingChange[1])
|
||||
} else {
|
||||
analyzed.ParsedBreakingChangeMessage = breakingChange[0]
|
||||
}
|
||||
a.log.Infof(" %s, BREAKING CHANGE found", commit.Message)
|
||||
return analyzed, true, nil
|
||||
}
|
||||
}
|
||||
a.log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag)
|
||||
return analyzed, false, fmt.Errorf("not found")
|
||||
|
||||
}
|
||||
261
internal/analyzer/conventional_test.go
Normal file
261
internal/analyzer/conventional_test.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package analyzer_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Nightapes/go-semantic-release/internal/analyzer"
|
||||
"github.com/Nightapes/go-semantic-release/internal/shared"
|
||||
"github.com/Nightapes/go-semantic-release/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConventional(t *testing.T) {
|
||||
|
||||
testConfigs := []struct {
|
||||
testCase string
|
||||
commits []shared.Commit
|
||||
analyzedCommits map[shared.Release][]shared.AnalyzedCommit
|
||||
}{
|
||||
{
|
||||
testCase: "feat",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
Scope: "internal/changelog",
|
||||
ParsedMessage: "my first commit",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
},
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat: no scope",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "no scope",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"major": {},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
{
|
||||
Message: "feat(internal/changelog): my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
{
|
||||
Message: "feat: no scope",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testCase: "feat breaking change",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "my first commit",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"major": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat!: my first break",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
ParsedBreakingChangeMessage: "my first break",
|
||||
},
|
||||
},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
{
|
||||
Message: "feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
{
|
||||
Message: "feat!: my first break",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testCase: "feat breaking change footer",
|
||||
commits: []shared.Commit{
|
||||
{
|
||||
Message: "feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
{
|
||||
Message: "feat: my first break \n\nBREAKING CHANGE: change api to v2\n",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
{
|
||||
Message: "feat!: my first break \n\nBREAKING CHANGE: hey from the change",
|
||||
Author: "me",
|
||||
Hash: "12345669",
|
||||
},
|
||||
},
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "my first commit",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"major": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat: my first break \n\nBREAKING CHANGE: change api to v2\n",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "my first break",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
ParsedBreakingChangeMessage: "change api to v2",
|
||||
},
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat!: my first break \n\nBREAKING CHANGE: hey from the change",
|
||||
Author: "me",
|
||||
Hash: "12345669",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "my first break",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
ParsedBreakingChangeMessage: "hey from the change",
|
||||
},
|
||||
},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
testCase: "invalid",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": {},
|
||||
"major": {},
|
||||
"patch": {},
|
||||
"none": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
{
|
||||
Message: "internal/changelog: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
{
|
||||
Message: "Merge feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testCase: "feat and build",
|
||||
analyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
|
||||
"minor": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "my first commit",
|
||||
Tag: "feat",
|
||||
TagString: "Features",
|
||||
Print: true,
|
||||
},
|
||||
},
|
||||
"none": {
|
||||
{
|
||||
Commit: shared.Commit{
|
||||
Message: "build: my first build",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
Scope: "",
|
||||
ParsedMessage: "my first build",
|
||||
Tag: "build",
|
||||
TagString: "Changes to CI/CD",
|
||||
Print: false,
|
||||
ParsedBreakingChangeMessage: "",
|
||||
},
|
||||
},
|
||||
"patch": {},
|
||||
"major": {},
|
||||
},
|
||||
commits: []shared.Commit{
|
||||
{
|
||||
Message: "feat: my first commit",
|
||||
Author: "me",
|
||||
Hash: "12345667",
|
||||
},
|
||||
{
|
||||
Message: "build: my first build",
|
||||
Author: "me",
|
||||
Hash: "12345668",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conventional, err := analyzer.New("conventional", config.ChangelogConfig{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, test := range testConfigs {
|
||||
analyzedCommits := conventional.Analyze(test.commits)
|
||||
assert.Equalf(t, test.analyzedCommits["major"], analyzedCommits["major"], "Testcase %s should have major commits", test.testCase)
|
||||
assert.Equalf(t, test.analyzedCommits["minor"], analyzedCommits["minor"], "Testcase %s should have minor commits", test.testCase)
|
||||
assert.Equalf(t, test.analyzedCommits["patch"], analyzedCommits["patch"], "Testcase %s should have patch commits", test.testCase)
|
||||
assert.Equalf(t, test.analyzedCommits["none"], analyzedCommits["none"], "Testcase %s should have none commits", test.testCase)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user