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]")
+}