1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-07-15 01:34:21 +02:00

feat(changelog): custom commit format (#4802)

This allows to use templates for commit messages in the changelog when
using `github`, `gitea`, or `gitlab` as the changelog implementation.

closes #4800

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker
2024-04-24 09:08:20 -03:00
committed by GitHub
parent 2c93bd7c7f
commit 39bf6668bc
14 changed files with 127 additions and 42 deletions

View File

@ -56,11 +56,20 @@ type Client interface {
CreateRelease(ctx *context.Context, body string) (releaseID string, err error) CreateRelease(ctx *context.Context, body string) (releaseID string, err error)
PublishRelease(ctx *context.Context, releaseID string) (err error) PublishRelease(ctx *context.Context, releaseID string) (err error)
Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) (err error) Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) (err error)
Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) Changelog(ctx *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error)
ReleaseURLTemplater ReleaseURLTemplater
FileCreator FileCreator
} }
// ChangelogItem represents a changelog item, basically, a commit and its author.
type ChangelogItem struct {
SHA string
Message string
AuthorName string
AuthorEmail string
AuthorUsername string
}
// ReleaseURLTemplater provides the release URL as a template, containing the // ReleaseURLTemplater provides the release URL as a template, containing the
// artifact name as well. // artifact name as well.
type ReleaseURLTemplater interface { type ReleaseURLTemplater interface {

View File

@ -75,22 +75,23 @@ func newGitea(ctx *context.Context, token string) (*giteaClient, error) {
} }
// Changelog fetches the changelog between two revisions. // Changelog fetches the changelog between two revisions.
func (c *giteaClient) Changelog(_ *context.Context, repo Repo, prev, current string) (string, error) { func (c *giteaClient) Changelog(_ *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error) {
result, _, err := c.client.CompareCommits(repo.Owner, repo.Name, prev, current) result, _, err := c.client.CompareCommits(repo.Owner, repo.Name, prev, current)
if err != nil { if err != nil {
return "", err return nil, err
} }
var log []string var log []ChangelogItem
for _, commit := range result.Commits { for _, commit := range result.Commits {
log = append(log, fmt.Sprintf( log = append(log, ChangelogItem{
"%s: %s (@%s)", SHA: commit.SHA[:7],
commit.SHA[:7], Message: strings.Split(commit.RepoCommit.Message, "\n")[0],
strings.Split(commit.RepoCommit.Message, "\n")[0], AuthorName: commit.Author.FullName,
commit.Author.UserName, AuthorEmail: commit.Author.Email,
)) AuthorUsername: commit.Author.UserName,
})
} }
return strings.Join(log, "\n"), nil return log, nil
} }
// CloseMilestone closes a given milestone. // CloseMilestone closes a given milestone.

View File

@ -617,6 +617,8 @@ func TestGiteaChangelog(t *testing.T) {
}, },
Author: &gitea.User{ Author: &gitea.User{
UserName: "johndoe", UserName: "johndoe",
FullName: "John Doe",
Email: "nope@nope.nope",
}, },
RepoCommit: &gitea.RepoCommit{ RepoCommit: &gitea.RepoCommit{
Message: "feat: impl something\n\nnsome other lines", Message: "feat: impl something\n\nnsome other lines",
@ -646,7 +648,15 @@ func TestGiteaChangelog(t *testing.T) {
result, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0") result, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "c8488dc: feat: impl something (@johndoe)", result) require.Equal(t, []ChangelogItem{
{
SHA: "c8488dc",
Message: "feat: impl something",
AuthorUsername: "johndoe",
AuthorName: "John Doe",
AuthorEmail: "nope@nope.nope",
},
}, result)
} }
func TestGiteatGetInstanceURL(t *testing.T) { func TestGiteatGetInstanceURL(t *testing.T) {

View File

@ -101,23 +101,24 @@ func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, pre
return notes.Body, err return notes.Body, err
} }
func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) { func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error) {
c.checkRateLimit(ctx) c.checkRateLimit(ctx)
var log []string var log []ChangelogItem
opts := &github.ListOptions{PerPage: 100} opts := &github.ListOptions{PerPage: 100}
for { for {
result, resp, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current, opts) result, resp, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current, opts)
if err != nil { if err != nil {
return "", err return nil, err
} }
for _, commit := range result.Commits { for _, commit := range result.Commits {
log = append(log, fmt.Sprintf( log = append(log, ChangelogItem{
"%s: %s (@%s)", SHA: commit.GetSHA(),
commit.GetSHA(), Message: strings.Split(commit.Commit.GetMessage(), "\n")[0],
strings.Split(commit.Commit.GetMessage(), "\n")[0], AuthorName: commit.GetAuthor().GetName(),
commit.GetAuthor().GetLogin(), AuthorEmail: commit.GetAuthor().GetEmail(),
)) AuthorUsername: commit.GetAuthor().GetLogin(),
})
} }
if resp.NextPage == 0 { if resp.NextPage == 0 {
break break
@ -125,7 +126,7 @@ func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current
opts.Page = resp.NextPage opts.Page = resp.NextPage
} }
return strings.Join(log, "\n"), nil return log, nil
} }
// getDefaultBranch returns the default branch of a github repo // getDefaultBranch returns the default branch of a github repo

View File

@ -300,7 +300,15 @@ func TestGitHubChangelog(t *testing.T) {
log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0") log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "6dcb09b5b57875f334f61aebed695e2e4193db5e: Fix all the bugs (@octocat)", log) require.Equal(t, []ChangelogItem{
{
SHA: "6dcb09b5b57875f334f61aebed695e2e4193db5e",
Message: "Fix all the bugs",
AuthorName: "Octocat",
AuthorEmail: "octo@cat",
AuthorUsername: "octocat",
},
}, log)
} }
func TestGitHubReleaseNotes(t *testing.T) { func TestGitHubReleaseNotes(t *testing.T) {

View File

@ -64,27 +64,26 @@ func newGitLab(ctx *context.Context, token string) (*gitlabClient, error) {
return &gitlabClient{client: client}, nil return &gitlabClient{client: client}, nil
} }
func (c *gitlabClient) Changelog(_ *context.Context, repo Repo, prev, current string) (string, error) { func (c *gitlabClient) Changelog(_ *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error) {
cmpOpts := &gitlab.CompareOptions{ cmpOpts := &gitlab.CompareOptions{
From: &prev, From: &prev,
To: &current, To: &current,
} }
result, _, err := c.client.Repositories.Compare(repo.String(), cmpOpts) result, _, err := c.client.Repositories.Compare(repo.String(), cmpOpts)
var log []string var log []ChangelogItem
if err != nil { if err != nil {
return "", err return nil, err
} }
for _, commit := range result.Commits { for _, commit := range result.Commits {
log = append(log, fmt.Sprintf( log = append(log, ChangelogItem{
"%s: %s (%s <%s>)", SHA: commit.ShortID,
commit.ShortID, Message: strings.Split(commit.Message, "\n")[0],
strings.Split(commit.Message, "\n")[0], AuthorName: commit.AuthorName,
commit.AuthorName, AuthorEmail: commit.AuthorEmail,
commit.AuthorEmail, })
))
} }
return strings.Join(log, "\n"), nil return log, nil
} }
// getDefaultBranch get the default branch // getDefaultBranch get the default branch

View File

@ -484,7 +484,15 @@ func TestGitLabChangelog(t *testing.T) {
log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0") log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "6dcb09b5: Fix all the bugs (Joey User <joey@user.edu>)", log) require.Equal(t, []ChangelogItem{
{
SHA: "6dcb09b5",
Message: "Fix all the bugs",
AuthorName: "Joey User",
AuthorEmail: "joey@user.edu",
AuthorUsername: "",
},
}, log)
} }
func TestGitLabCreateFile(t *testing.T) { func TestGitLabCreateFile(t *testing.T) {

View File

@ -39,7 +39,7 @@ type Mock struct {
Lock sync.Mutex Lock sync.Mutex
ClosedMilestone string ClosedMilestone string
FailToCloseMilestone bool FailToCloseMilestone bool
Changes string Changes []ChangelogItem
ReleaseNotes string ReleaseNotes string
ReleaseNotesParams []string ReleaseNotesParams []string
OpenedPullRequest bool OpenedPullRequest bool
@ -56,11 +56,11 @@ func (c *Mock) OpenPullRequest(_ *context.Context, _, _ Repo, _ string, _ bool)
return nil return nil
} }
func (c *Mock) Changelog(_ *context.Context, _ Repo, _, _ string) (string, error) { func (c *Mock) Changelog(_ *context.Context, _ Repo, _, _ string) ([]ChangelogItem, error) {
if c.Changes != "" { if len(c.Changes) > 0 {
return c.Changes, nil return c.Changes, nil
} }
return "", ErrNotImplemented return nil, ErrNotImplemented
} }
func (c *Mock) GenerateReleaseNotes(_ *context.Context, _ Repo, prev, current string) (string, error) { func (c *Mock) GenerateReleaseNotes(_ *context.Context, _ Repo, prev, current string) (string, error) {

View File

@ -6,7 +6,9 @@
"message": "Fix all the bugs\nlalalal" "message": "Fix all the bugs\nlalalal"
}, },
"author": { "author": {
"login": "octocat" "login": "octocat",
"name": "Octocat",
"email": "octo@cat"
} }
} }
] ]

View File

@ -41,6 +41,7 @@ const (
type Pipe struct{} type Pipe struct{}
func (Pipe) String() string { return "generating changelog" } func (Pipe) String() string { return "generating changelog" }
func (Pipe) Skip(ctx *context.Context) (bool, error) { func (Pipe) Skip(ctx *context.Context) (bool, error) {
if ctx.Snapshot { if ctx.Snapshot {
return true, nil return true, nil
@ -53,6 +54,13 @@ func (Pipe) Skip(ctx *context.Context) (bool, error) {
return tmpl.New(ctx).Bool(ctx.Config.Changelog.Disable) return tmpl.New(ctx).Bool(ctx.Config.Changelog.Disable)
} }
func (Pipe) Default(ctx *context.Context) error {
if ctx.Config.Changelog.Format == "" {
ctx.Config.Changelog.Format = "{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})"
}
return nil
}
// Run the pipe. // Run the pipe.
func (Pipe) Run(ctx *context.Context) error { func (Pipe) Run(ctx *context.Context) error {
notes, err := loadContent(ctx, ctx.ReleaseNotesFile, ctx.ReleaseNotesTmpl) notes, err := loadContent(ctx, ctx.ReleaseNotesFile, ctx.ReleaseNotesTmpl)
@ -445,7 +453,25 @@ type scmChangeloger struct {
func (c *scmChangeloger) Log(ctx *context.Context) (string, error) { func (c *scmChangeloger) Log(ctx *context.Context) (string, error) {
prev, current := comparePair(ctx) prev, current := comparePair(ctx)
return c.client.Changelog(ctx, c.repo, prev, current) items, err := c.client.Changelog(ctx, c.repo, prev, current)
if err != nil {
return "", err
}
var lines []string
for _, item := range items {
line, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
"SHA": item.SHA,
"Message": item.Message,
"AuthorUsername": item.AuthorUsername,
"AuthorName": item.AuthorName,
"AuthorEmail": item.AuthorEmail,
}).Apply(ctx.Config.Changelog.Format)
if err != nil {
return "", err
}
lines = append(lines, line)
}
return strings.Join(lines, "\n"), nil
} }
type githubNativeChangeloger struct { type githubNativeChangeloger struct {

View File

@ -515,10 +515,19 @@ func TestGetChangelogGitHub(t *testing.T) {
Use: useGitHub, Use: useGitHub,
}, },
}, testctx.WithCurrentTag("v0.180.2"), testctx.WithPreviousTag("v0.180.1")) }, testctx.WithCurrentTag("v0.180.2"), testctx.WithPreviousTag("v0.180.1"))
require.NoError(t, Pipe{}.Default(ctx))
expected := "c90f1085f255d0af0b055160bfff5ee40f47af79: fix: do not skip any defaults (#2521) (@caarlos0)" expected := "c90f1085f255d0af0b055160bfff5ee40f47af79: fix: do not skip any defaults (#2521) (@caarlos0)"
mock := client.NewMock() mock := client.NewMock()
mock.Changes = expected mock.Changes = []client.ChangelogItem{
{
SHA: "c90f1085f255d0af0b055160bfff5ee40f47af79",
Message: "fix: do not skip any defaults (#2521)",
AuthorName: "Carlos",
AuthorEmail: "nope@nope.com",
AuthorUsername: "caarlos0",
},
}
l := scmChangeloger{ l := scmChangeloger{
client: mock, client: mock,
repo: client.Repo{ repo: client.Repo{

View File

@ -1111,6 +1111,7 @@ type Changelog struct {
Sort string `yaml:"sort,omitempty" json:"sort,omitempty" jsonschema:"enum=asc,enum=desc,enum=,default="` Sort string `yaml:"sort,omitempty" json:"sort,omitempty" jsonschema:"enum=asc,enum=desc,enum=,default="`
Disable string `yaml:"disable,omitempty" json:"disable,omitempty" jsonschema:"oneof_type=string;boolean"` Disable string `yaml:"disable,omitempty" json:"disable,omitempty" jsonschema:"oneof_type=string;boolean"`
Use string `yaml:"use,omitempty" json:"use,omitempty" jsonschema:"enum=git,enum=github,enum=github-native,enum=gitlab,default=git"` Use string `yaml:"use,omitempty" json:"use,omitempty" jsonschema:"enum=git,enum=github,enum=github-native,enum=gitlab,default=git"`
Format string `yaml:"format,omitempty" json:"omitempty"`
Groups []ChangelogGroup `yaml:"groups,omitempty" json:"groups,omitempty"` Groups []ChangelogGroup `yaml:"groups,omitempty" json:"groups,omitempty"`
Abbrev int `yaml:"abbrev,omitempty" json:"abbrev,omitempty"` Abbrev int `yaml:"abbrev,omitempty" json:"abbrev,omitempty"`

View File

@ -12,6 +12,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/bluesky" "github.com/goreleaser/goreleaser/internal/pipe/bluesky"
"github.com/goreleaser/goreleaser/internal/pipe/brew" "github.com/goreleaser/goreleaser/internal/pipe/brew"
"github.com/goreleaser/goreleaser/internal/pipe/build" "github.com/goreleaser/goreleaser/internal/pipe/build"
"github.com/goreleaser/goreleaser/internal/pipe/changelog"
"github.com/goreleaser/goreleaser/internal/pipe/checksums" "github.com/goreleaser/goreleaser/internal/pipe/checksums"
"github.com/goreleaser/goreleaser/internal/pipe/chocolatey" "github.com/goreleaser/goreleaser/internal/pipe/chocolatey"
"github.com/goreleaser/goreleaser/internal/pipe/discord" "github.com/goreleaser/goreleaser/internal/pipe/discord"
@ -64,6 +65,7 @@ var Defaulters = []Defaulter{
snapshot.Pipe{}, snapshot.Pipe{},
release.Pipe{}, release.Pipe{},
project.Pipe{}, project.Pipe{},
changelog.Pipe{},
gomod.Pipe{}, gomod.Pipe{},
build.Pipe{}, build.Pipe{},
universalbinary.Pipe{}, universalbinary.Pipe{},

View File

@ -27,6 +27,15 @@ changelog:
# Default: 'git' # Default: 'git'
use: github use: github
# Format to use for commit formatting.
# Only available when use is one of `github`, `gitea`, or `gitlab`.
#
# Default: '{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})'
# Extra template fields: `SHA`, `Message`, `AuthorName`, `AuthorEmail`, and
# `AuthorUsername`.
# Since: v1.26
format: "{{.SHA}}: {{.Message}} (@{{.AuthorUsername}})"
# Sorts the changelog by the commit's messages. # Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty # Could either be asc, desc or empty
# Empty means 'no sorting', it'll use the output of `git log` as is. # Empty means 'no sorting', it'll use the output of `git log` as is.