diff --git a/go.mod b/go.mod index b215b084135b2637be7ce70d6b0a93be926951af..3aa179bc4cfa6833a4b8c9e3d5a6fc4d9eb6c3c5 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,14 @@ module github.com/go-semantic-release/changelog-generator-default go 1.19 -require github.com/go-semantic-release/semantic-release/v2 v2.25.0 +require ( + github.com/go-semantic-release/semantic-release/v2 v2.25.0 + github.com/stretchr/testify v1.8.1 +) require ( github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -22,6 +26,7 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.6.1 // indirect diff --git a/pkg/generator/changelog_generator.go b/pkg/generator/changelog_generator.go index 86e1564b03419d6ff342591ee6abf595981b3c88..92f5d1908dfcd3960f334cd70320583459562d16 100644 --- a/pkg/generator/changelog_generator.go +++ b/pkg/generator/changelog_generator.go @@ -1,8 +1,10 @@ package generator import ( + "bytes" "fmt" "strings" + "text/template" "time" "github.com/go-semantic-release/semantic-release/v2/pkg/generator" @@ -16,32 +18,49 @@ func trimSHA(sha string) string { return sha[:8] } -func formatCommit(c *semrel.Commit) string { - ret := "* " - if c.Scope != "" { - ret += fmt.Sprintf("**%s:** ", c.Scope) +var templateFuncMap = template.FuncMap{ + "trimSHA": trimSHA, +} + +var defaultFormatCommitTemplateStr = `* {{with .Scope -}} **{{.}}:** {{end}} {{- .Message}} ({{trimSHA .SHA}})` + +func formatCommit(tpl *template.Template, c *semrel.Commit) string { + ret := &bytes.Buffer{} + err := tpl.Execute(ret, c) + if err != nil { + panic(err) } - ret += fmt.Sprintf("%s (%s)", c.Message, trimSHA(c.SHA)) - return ret + return ret.String() } var CGVERSION = "dev" type DefaultChangelogGenerator struct { - emojis bool + emojis bool + formatCommitTpl *template.Template } func (g *DefaultChangelogGenerator) Init(m map[string]string) error { emojis := false - emojiConfig := m["emojis"] - if emojiConfig == "true" { emojis = true } - g.emojis = emojis + templateStr := defaultFormatCommitTemplateStr + if tplStr := m["format_commit_template"]; tplStr != "" { + templateStr = tplStr + } + + parsedTemplate, err := template.New("commit-template"). + Funcs(templateFuncMap). + Parse(templateStr) + if err != nil { + return fmt.Errorf("failed to parse commit template: %w", err) + } + g.formatCommitTpl = parsedTemplate + return nil } @@ -61,14 +80,14 @@ func (g *DefaultChangelogGenerator) Generate(changelogConfig *generator.Changelo break } if commit.Change != nil && commit.Change.Major { - bc := fmt.Sprintf("%s\n```\n%s\n```", formatCommit(commit), strings.Join(commit.Raw[1:], "\n")) + bc := fmt.Sprintf("%s\n```\n%s\n```", formatCommit(g.formatCommitTpl, commit), strings.Join(commit.Raw[1:], "\n")) clTypes.AppendContent("%%bc%%", bc) continue } if commit.Type == "" { continue } - clTypes.AppendContent(commit.Type, formatCommit(commit)) + clTypes.AppendContent(commit.Type, formatCommit(g.formatCommitTpl, commit)) } for _, ct := range clTypes { if ct.Content == "" { diff --git a/pkg/generator/changelog_generator_test.go b/pkg/generator/changelog_generator_test.go index 57f23995381c34b5497d7680700cba247b9cefec..5e41c1dd6941d8785e5a9d4995a31017884959b0 100644 --- a/pkg/generator/changelog_generator_test.go +++ b/pkg/generator/changelog_generator_test.go @@ -1,68 +1,125 @@ package generator import ( - "strings" "testing" + "text/template" "github.com/go-semantic-release/semantic-release/v2/pkg/generator" "github.com/go-semantic-release/semantic-release/v2/pkg/semrel" + "github.com/stretchr/testify/require" ) +var testCommits = []*semrel.Commit{ + {}, + { + SHA: "123456789", Type: "feat", Scope: "app", Message: "commit message", + Annotations: map[string]string{"author_login": "test"}, + }, + { + SHA: "deadbeef", Type: "fix", Scope: "", Message: "commit message", + Annotations: map[string]string{"author_login": "test"}, + }, + { + SHA: "87654321", Type: "ci", Scope: "", Message: "commit message", + Annotations: map[string]string{"author_login": "test"}, + }, + { + SHA: "43218765", Type: "build", Scope: "", Message: "commit message", + Annotations: map[string]string{"author_login": "test"}, + }, + { + SHA: "12345678", Type: "yolo", Scope: "swag", Message: "commit message", + }, + { + SHA: "12345678", Type: "chore", Scope: "", Message: "commit message", + Raw: []string{"", "BREAKING CHANGE: test"}, + Change: &semrel.Change{Major: true}, + Annotations: map[string]string{"author_login": "test"}, + }, + { + SHA: "12345679", Type: "chore!", Scope: "user", Message: "another commit message", + Raw: []string{"another commit message", "changed ID int into UUID"}, + Change: &semrel.Change{Major: true}, + }, + { + SHA: "stop", Type: "chore", Scope: "", Message: "not included", + }, +} + +var testChangelogConfig = &generator.ChangelogGeneratorConfig{ + Commits: testCommits, + LatestRelease: &semrel.Release{SHA: "stop"}, + NewVersion: "2.0.0", +} + func TestDefaultGenerator(t *testing.T) { - changelogConfig := &generator.ChangelogGeneratorConfig{} - changelogConfig.Commits = []*semrel.Commit{ - {}, - {SHA: "123456789", Type: "feat", Scope: "app", Message: "commit message"}, - {SHA: "abcd", Type: "fix", Scope: "", Message: "commit message"}, - {SHA: "87654321", Type: "ci", Scope: "", Message: "commit message"}, - {SHA: "43218765", Type: "build", Scope: "", Message: "commit message"}, - {SHA: "12345678", Type: "yolo", Scope: "swag", Message: "commit message"}, - {SHA: "12345678", Type: "chore", Scope: "", Message: "commit message", Raw: []string{"", "BREAKING CHANGE: test"}, Change: &semrel.Change{Major: true}}, - {SHA: "12345679", Type: "chore!", Scope: "user", Message: "another commit message", Raw: []string{"another commit message", "changed ID int into UUID"}, Change: &semrel.Change{Major: true}}, - {SHA: "stop", Type: "chore", Scope: "", Message: "not included"}, - } - changelogConfig.LatestRelease = &semrel.Release{SHA: "stop"} - changelogConfig.NewVersion = "2.0.0" - generator := &DefaultChangelogGenerator{} - changelog := generator.Generate(changelogConfig) - if !strings.Contains(changelog, "* **app:** commit message (12345678)") || - !strings.Contains(changelog, "* commit message (abcd)") || - !strings.Contains(changelog, "#### yolo") || - !strings.Contains(changelog, "#### Build") || - !strings.Contains(changelog, "#### CI") || - !strings.Contains(changelog, "```\nBREAKING CHANGE: test\n```") || - strings.Contains(changelog, "not included") { - t.Fail() - } + clGen := &DefaultChangelogGenerator{} + require.NoError(t, clGen.Init(map[string]string{})) + changelog := clGen.Generate(testChangelogConfig) + + require.Contains(t, changelog, "* **app:** commit message (12345678)") + require.Contains(t, changelog, "* commit message (deadbeef)") + require.Contains(t, changelog, "#### yolo") + require.Contains(t, changelog, "#### Build") + require.Contains(t, changelog, "#### CI") + require.Contains(t, changelog, "```\nBREAKING CHANGE: test\n```") + require.NotContains(t, changelog, "not included") } func TestEmojiGenerator(t *testing.T) { - changelogConfig := &generator.ChangelogGeneratorConfig{} - changelogConfig.Commits = []*semrel.Commit{ - {}, - {SHA: "123456789", Type: "feat", Scope: "app", Message: "commit message"}, - {SHA: "abcd", Type: "fix", Scope: "", Message: "commit message"}, - {SHA: "87654321", Type: "ci", Scope: "", Message: "commit message"}, - {SHA: "43218765", Type: "build", Scope: "", Message: "commit message"}, - {SHA: "12345678", Type: "yolo", Scope: "swag", Message: "commit message"}, - {SHA: "12345678", Type: "chore", Scope: "", Message: "commit message", Raw: []string{"", "BREAKING CHANGE: test"}, Change: &semrel.Change{Major: true}}, - {SHA: "12345679", Type: "chore!", Scope: "user", Message: "another commit message", Raw: []string{"another commit message", "changed ID int into UUID"}, Change: &semrel.Change{Major: true}}, - {SHA: "stop", Type: "chore", Scope: "", Message: "not included"}, + clGen := &DefaultChangelogGenerator{} + require.NoError(t, clGen.Init(map[string]string{"emojis": "true"})) + changelog := clGen.Generate(testChangelogConfig) + + require.Contains(t, changelog, "* **app:** commit message (12345678)") + require.Contains(t, changelog, "* commit message (deadbeef)") + require.Contains(t, changelog, "#### 🎁 Feature") + require.Contains(t, changelog, "#### 🐞 Bug Fixes") + require.Contains(t, changelog, "#### 🔁 CI") + require.Contains(t, changelog, "#### 📦 Build") + require.Contains(t, changelog, "#### 📣 Breaking Changes") + require.Contains(t, changelog, "#### yolo") + require.Contains(t, changelog, "```\nBREAKING CHANGE: test\n```") + require.NotContains(t, changelog, "not included") +} + +func TestFormatCommit(t *testing.T) { + testCases := []struct { + tpl string + commit *semrel.Commit + expectedOutput string + }{ + { + tpl: defaultFormatCommitTemplateStr, + commit: &semrel.Commit{SHA: "123456789", Type: "feat", Scope: "", Message: "commit message"}, + expectedOutput: "* commit message (12345678)", + }, + { + tpl: defaultFormatCommitTemplateStr, + commit: &semrel.Commit{SHA: "123", Type: "feat", Scope: "app", Message: "commit message"}, + expectedOutput: "* **app:** commit message (123)", + }, + { + tpl: `* {{.SHA}} - {{.Message}} {{- with index .Annotations "author_login" }} [by @{{.}}] {{- end}}`, + commit: &semrel.Commit{SHA: "deadbeef", Type: "fix", Message: "custom template", Annotations: map[string]string{"author_login": "test"}}, + expectedOutput: "* deadbeef - custom template [by @test]", + }, } - changelogConfig.LatestRelease = &semrel.Release{SHA: "stop"} - changelogConfig.NewVersion = "2.0.0" - generator := &DefaultChangelogGenerator{emojis: true} - changelog := generator.Generate(changelogConfig) - if !strings.Contains(changelog, "* **app:** commit message (12345678)") || - !strings.Contains(changelog, "* commit message (abcd)") || - !strings.Contains(changelog, "#### 🎁 Feature") || - !strings.Contains(changelog, "#### 🐞 Bug Fixes") || - !strings.Contains(changelog, "#### 🔁 CI") || - !strings.Contains(changelog, "#### 📦 Build") || - !strings.Contains(changelog, "#### 📣 Breaking Changes") || - !strings.Contains(changelog, "#### yolo") || - !strings.Contains(changelog, "```\nBREAKING CHANGE: test\n```") || - strings.Contains(changelog, "not included") { - t.Fail() + for _, tc := range testCases { + t.Run(tc.expectedOutput, func(t *testing.T) { + tpl := template.Must(template.New("test").Funcs(templateFuncMap).Parse(tc.tpl)) + output := formatCommit(tpl, tc.commit) + require.Equal(t, tc.expectedOutput, output) + }) } } + +func TestFormatCommitWithCustomTemplate(t *testing.T) { + clGen := &DefaultChangelogGenerator{} + require.NoError(t, clGen.Init(map[string]string{ + "format_commit_template": "* `{{ trimSHA .SHA}}` - {{.Message}} {{- with index .Annotations \"author_login\" }} [by @{{.}}] {{- end}}", + })) + changelog := clGen.Generate(testChangelogConfig) + require.Contains(t, changelog, "* `12345678` - commit message [by @test]") + require.NotContains(t, changelog, "* `deadbeef` - commit message (deadbeef) [by @test]") +}