From 39bf6668bc086d1c110e9321f7303da3de7e22b7 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 24 Apr 2024 09:08:20 -0300 Subject: [PATCH] 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 --- internal/client/client.go | 11 +++++++- internal/client/gitea.go | 21 ++++++++------- internal/client/gitea_test.go | 12 ++++++++- internal/client/github.go | 21 ++++++++------- internal/client/github_test.go | 10 ++++++- internal/client/gitlab.go | 21 +++++++-------- internal/client/gitlab_test.go | 10 ++++++- internal/client/mock.go | 8 +++--- internal/client/testdata/github/compare.json | 4 ++- internal/pipe/changelog/changelog.go | 28 +++++++++++++++++++- internal/pipe/changelog/changelog_test.go | 11 +++++++- pkg/config/config.go | 1 + pkg/defaults/defaults.go | 2 ++ www/docs/customization/changelog.md | 9 +++++++ 14 files changed, 127 insertions(+), 42 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 13caec849..8b51efdf2 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -56,11 +56,20 @@ type Client interface { CreateRelease(ctx *context.Context, body string) (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) - Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) + Changelog(ctx *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error) ReleaseURLTemplater 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 // artifact name as well. type ReleaseURLTemplater interface { diff --git a/internal/client/gitea.go b/internal/client/gitea.go index 7f5713cb5..353c65c67 100644 --- a/internal/client/gitea.go +++ b/internal/client/gitea.go @@ -75,22 +75,23 @@ func newGitea(ctx *context.Context, token string) (*giteaClient, error) { } // 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) if err != nil { - return "", err + return nil, err } - var log []string + var log []ChangelogItem for _, commit := range result.Commits { - log = append(log, fmt.Sprintf( - "%s: %s (@%s)", - commit.SHA[:7], - strings.Split(commit.RepoCommit.Message, "\n")[0], - commit.Author.UserName, - )) + log = append(log, ChangelogItem{ + SHA: commit.SHA[:7], + Message: strings.Split(commit.RepoCommit.Message, "\n")[0], + AuthorName: commit.Author.FullName, + AuthorEmail: commit.Author.Email, + AuthorUsername: commit.Author.UserName, + }) } - return strings.Join(log, "\n"), nil + return log, nil } // CloseMilestone closes a given milestone. diff --git a/internal/client/gitea_test.go b/internal/client/gitea_test.go index a4cc57195..98b125275 100644 --- a/internal/client/gitea_test.go +++ b/internal/client/gitea_test.go @@ -617,6 +617,8 @@ func TestGiteaChangelog(t *testing.T) { }, Author: &gitea.User{ UserName: "johndoe", + FullName: "John Doe", + Email: "nope@nope.nope", }, RepoCommit: &gitea.RepoCommit{ 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") 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) { diff --git a/internal/client/github.go b/internal/client/github.go index c92997e98..d2b79d9c3 100644 --- a/internal/client/github.go +++ b/internal/client/github.go @@ -101,23 +101,24 @@ func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, pre 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) - var log []string + var log []ChangelogItem opts := &github.ListOptions{PerPage: 100} for { result, resp, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current, opts) if err != nil { - return "", err + return nil, err } for _, commit := range result.Commits { - log = append(log, fmt.Sprintf( - "%s: %s (@%s)", - commit.GetSHA(), - strings.Split(commit.Commit.GetMessage(), "\n")[0], - commit.GetAuthor().GetLogin(), - )) + log = append(log, ChangelogItem{ + SHA: commit.GetSHA(), + Message: strings.Split(commit.Commit.GetMessage(), "\n")[0], + AuthorName: commit.GetAuthor().GetName(), + AuthorEmail: commit.GetAuthor().GetEmail(), + AuthorUsername: commit.GetAuthor().GetLogin(), + }) } if resp.NextPage == 0 { break @@ -125,7 +126,7 @@ func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current opts.Page = resp.NextPage } - return strings.Join(log, "\n"), nil + return log, nil } // getDefaultBranch returns the default branch of a github repo diff --git a/internal/client/github_test.go b/internal/client/github_test.go index d3d209fa2..66308e3ea 100644 --- a/internal/client/github_test.go +++ b/internal/client/github_test.go @@ -300,7 +300,15 @@ func TestGitHubChangelog(t *testing.T) { log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0") 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) { diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go index e7597b306..a3a8bb505 100644 --- a/internal/client/gitlab.go +++ b/internal/client/gitlab.go @@ -64,27 +64,26 @@ func newGitLab(ctx *context.Context, token string) (*gitlabClient, error) { 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{ From: &prev, To: ¤t, } result, _, err := c.client.Repositories.Compare(repo.String(), cmpOpts) - var log []string + var log []ChangelogItem if err != nil { - return "", err + return nil, err } for _, commit := range result.Commits { - log = append(log, fmt.Sprintf( - "%s: %s (%s <%s>)", - commit.ShortID, - strings.Split(commit.Message, "\n")[0], - commit.AuthorName, - commit.AuthorEmail, - )) + log = append(log, ChangelogItem{ + SHA: commit.ShortID, + Message: strings.Split(commit.Message, "\n")[0], + AuthorName: commit.AuthorName, + AuthorEmail: commit.AuthorEmail, + }) } - return strings.Join(log, "\n"), nil + return log, nil } // getDefaultBranch get the default branch diff --git a/internal/client/gitlab_test.go b/internal/client/gitlab_test.go index 8ccfc5847..6a48f344c 100644 --- a/internal/client/gitlab_test.go +++ b/internal/client/gitlab_test.go @@ -484,7 +484,15 @@ func TestGitLabChangelog(t *testing.T) { log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0") require.NoError(t, err) - require.Equal(t, "6dcb09b5: Fix all the bugs (Joey User )", 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) { diff --git a/internal/client/mock.go b/internal/client/mock.go index 222eb2d58..c669fafd2 100644 --- a/internal/client/mock.go +++ b/internal/client/mock.go @@ -39,7 +39,7 @@ type Mock struct { Lock sync.Mutex ClosedMilestone string FailToCloseMilestone bool - Changes string + Changes []ChangelogItem ReleaseNotes string ReleaseNotesParams []string OpenedPullRequest bool @@ -56,11 +56,11 @@ func (c *Mock) OpenPullRequest(_ *context.Context, _, _ Repo, _ string, _ bool) return nil } -func (c *Mock) Changelog(_ *context.Context, _ Repo, _, _ string) (string, error) { - if c.Changes != "" { +func (c *Mock) Changelog(_ *context.Context, _ Repo, _, _ string) ([]ChangelogItem, error) { + if len(c.Changes) > 0 { return c.Changes, nil } - return "", ErrNotImplemented + return nil, ErrNotImplemented } func (c *Mock) GenerateReleaseNotes(_ *context.Context, _ Repo, prev, current string) (string, error) { diff --git a/internal/client/testdata/github/compare.json b/internal/client/testdata/github/compare.json index d07478818..dfda1f275 100644 --- a/internal/client/testdata/github/compare.json +++ b/internal/client/testdata/github/compare.json @@ -6,7 +6,9 @@ "message": "Fix all the bugs\nlalalal" }, "author": { - "login": "octocat" + "login": "octocat", + "name": "Octocat", + "email": "octo@cat" } } ] diff --git a/internal/pipe/changelog/changelog.go b/internal/pipe/changelog/changelog.go index 4e2128741..98d820d17 100644 --- a/internal/pipe/changelog/changelog.go +++ b/internal/pipe/changelog/changelog.go @@ -41,6 +41,7 @@ const ( type Pipe struct{} func (Pipe) String() string { return "generating changelog" } + func (Pipe) Skip(ctx *context.Context) (bool, error) { if ctx.Snapshot { return true, nil @@ -53,6 +54,13 @@ func (Pipe) Skip(ctx *context.Context) (bool, error) { 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. func (Pipe) Run(ctx *context.Context) error { notes, err := loadContent(ctx, ctx.ReleaseNotesFile, ctx.ReleaseNotesTmpl) @@ -445,7 +453,25 @@ type scmChangeloger struct { func (c *scmChangeloger) Log(ctx *context.Context) (string, error) { 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 { diff --git a/internal/pipe/changelog/changelog_test.go b/internal/pipe/changelog/changelog_test.go index a98d65f48..73e79cf95 100644 --- a/internal/pipe/changelog/changelog_test.go +++ b/internal/pipe/changelog/changelog_test.go @@ -515,10 +515,19 @@ func TestGetChangelogGitHub(t *testing.T) { Use: useGitHub, }, }, 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)" 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{ client: mock, repo: client.Repo{ diff --git a/pkg/config/config.go b/pkg/config/config.go index 495d7d74a..ff1a27c5f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1111,6 +1111,7 @@ type Changelog struct { 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"` 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"` Abbrev int `yaml:"abbrev,omitempty" json:"abbrev,omitempty"` diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index db64233e9..3b0cbc4ae 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -12,6 +12,7 @@ import ( "github.com/goreleaser/goreleaser/internal/pipe/bluesky" "github.com/goreleaser/goreleaser/internal/pipe/brew" "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/chocolatey" "github.com/goreleaser/goreleaser/internal/pipe/discord" @@ -64,6 +65,7 @@ var Defaulters = []Defaulter{ snapshot.Pipe{}, release.Pipe{}, project.Pipe{}, + changelog.Pipe{}, gomod.Pipe{}, build.Pipe{}, universalbinary.Pipe{}, diff --git a/www/docs/customization/changelog.md b/www/docs/customization/changelog.md index ab6809fec..4e11c6764 100644 --- a/www/docs/customization/changelog.md +++ b/www/docs/customization/changelog.md @@ -27,6 +27,15 @@ changelog: # Default: 'git' 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. # Could either be asc, desc or empty # Empty means 'no sorting', it'll use the output of `git log` as is.