2017-10-16 00:21:35 +02:00
|
|
|
// Package changelog provides the release changelog to goreleaser.
|
|
|
|
package changelog
|
|
|
|
|
|
|
|
import (
|
2017-10-20 00:44:12 +02:00
|
|
|
"errors"
|
2017-10-16 00:21:35 +02:00
|
|
|
"fmt"
|
2020-01-31 20:38:56 +02:00
|
|
|
"os"
|
2018-05-02 05:54:16 +02:00
|
|
|
"path/filepath"
|
2017-10-18 03:45:19 +02:00
|
|
|
"regexp"
|
2017-10-20 00:44:12 +02:00
|
|
|
"sort"
|
2017-10-16 00:40:53 +02:00
|
|
|
"strings"
|
2017-10-16 00:21:35 +02:00
|
|
|
|
2018-05-02 05:54:16 +02:00
|
|
|
"github.com/apex/log"
|
2021-10-04 14:32:30 +02:00
|
|
|
"github.com/goreleaser/goreleaser/internal/client"
|
2017-10-16 00:21:35 +02:00
|
|
|
"github.com/goreleaser/goreleaser/internal/git"
|
2020-05-31 14:18:48 +02:00
|
|
|
"github.com/goreleaser/goreleaser/internal/tmpl"
|
2018-08-15 04:50:20 +02:00
|
|
|
"github.com/goreleaser/goreleaser/pkg/context"
|
2017-10-16 00:21:35 +02:00
|
|
|
)
|
|
|
|
|
2020-05-26 05:48:10 +02:00
|
|
|
// ErrInvalidSortDirection happens when the sort order is invalid.
|
2017-10-20 00:44:12 +02:00
|
|
|
var ErrInvalidSortDirection = errors.New("invalid sort direction")
|
|
|
|
|
2020-05-26 05:48:10 +02:00
|
|
|
// Pipe for checksums.
|
2017-10-16 00:21:35 +02:00
|
|
|
type Pipe struct{}
|
|
|
|
|
2021-09-18 15:21:29 +02:00
|
|
|
func (Pipe) String() string { return "generating changelog" }
|
|
|
|
func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Changelog.Skip || ctx.Snapshot }
|
2017-10-16 00:21:35 +02:00
|
|
|
|
2020-05-26 05:48:10 +02:00
|
|
|
// Run the pipe.
|
2017-10-16 00:33:39 +02:00
|
|
|
func (Pipe) Run(ctx *context.Context) error {
|
2021-05-22 02:07:47 +02:00
|
|
|
notes, err := loadContent(ctx, ctx.ReleaseNotesFile, ctx.ReleaseNotesTmpl)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-03-03 19:00:00 +02:00
|
|
|
}
|
2021-05-22 02:07:47 +02:00
|
|
|
ctx.ReleaseNotes = notes
|
|
|
|
|
2019-03-03 19:00:00 +02:00
|
|
|
if ctx.ReleaseNotes != "" {
|
|
|
|
return nil
|
|
|
|
}
|
2021-05-22 02:07:47 +02:00
|
|
|
|
|
|
|
footer, err := loadContent(ctx, ctx.ReleaseFooterFile, ctx.ReleaseFooterTmpl)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-11-15 15:22:11 +02:00
|
|
|
}
|
2021-05-22 02:07:47 +02:00
|
|
|
|
|
|
|
header, err := loadContent(ctx, ctx.ReleaseHeaderFile, ctx.ReleaseHeaderTmpl)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-11-15 15:22:11 +02:00
|
|
|
}
|
|
|
|
|
2017-10-20 00:44:12 +02:00
|
|
|
if err := checkSortDirection(ctx.Config.Changelog.Sort); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-11-15 15:22:11 +02:00
|
|
|
|
2017-10-20 00:44:12 +02:00
|
|
|
entries, err := buildChangelog(ctx)
|
2017-10-16 00:21:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-06-29 16:02:40 +02:00
|
|
|
|
|
|
|
changelogStringJoiner := "\n"
|
2019-08-26 09:31:38 +02:00
|
|
|
if ctx.TokenType == context.TokenTypeGitLab || ctx.TokenType == context.TokenTypeGitea {
|
|
|
|
// We need two or more whitespace to let markdown interpret
|
2019-06-29 16:02:40 +02:00
|
|
|
// it as newline. See https://docs.gitlab.com/ee/user/markdown.html#newlines for details
|
2019-08-26 09:31:38 +02:00
|
|
|
log.Debug("is gitlab or gitea changelog")
|
2019-06-29 16:02:40 +02:00
|
|
|
changelogStringJoiner = " \n"
|
|
|
|
}
|
2019-11-15 15:22:11 +02:00
|
|
|
|
2020-11-25 02:09:34 +02:00
|
|
|
changelogElements := []string{
|
|
|
|
"## Changelog",
|
|
|
|
strings.Join(entries, changelogStringJoiner),
|
|
|
|
}
|
2021-05-22 02:07:47 +02:00
|
|
|
if header != "" {
|
|
|
|
changelogElements = append([]string{header}, changelogElements...)
|
2020-11-25 02:09:34 +02:00
|
|
|
}
|
2021-05-22 02:07:47 +02:00
|
|
|
if footer != "" {
|
|
|
|
changelogElements = append(changelogElements, footer)
|
2020-11-25 02:09:34 +02:00
|
|
|
}
|
2020-11-25 04:39:59 +02:00
|
|
|
|
2020-11-25 02:09:34 +02:00
|
|
|
ctx.ReleaseNotes = strings.Join(changelogElements, "\n\n")
|
2020-11-25 04:39:59 +02:00
|
|
|
if !strings.HasSuffix(ctx.ReleaseNotes, "\n") {
|
|
|
|
ctx.ReleaseNotes += "\n"
|
|
|
|
}
|
2019-11-15 15:22:11 +02:00
|
|
|
|
2021-04-19 14:31:57 +02:00
|
|
|
path := filepath.Join(ctx.Config.Dist, "CHANGELOG.md")
|
2018-05-02 05:54:16 +02:00
|
|
|
log.WithField("changelog", path).Info("writing")
|
2021-04-25 18:00:51 +02:00
|
|
|
return os.WriteFile(path, []byte(ctx.ReleaseNotes), 0o644) //nolint: gosec
|
2017-10-20 00:44:12 +02:00
|
|
|
}
|
|
|
|
|
2019-01-22 05:56:16 +02:00
|
|
|
func loadFromFile(file string) (string, error) {
|
2021-04-25 18:00:51 +02:00
|
|
|
bts, err := os.ReadFile(file)
|
2019-01-22 05:56:16 +02:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return string(bts), nil
|
|
|
|
}
|
|
|
|
|
2017-10-20 00:44:12 +02:00
|
|
|
func checkSortDirection(mode string) error {
|
|
|
|
switch mode {
|
|
|
|
case "":
|
|
|
|
fallthrough
|
|
|
|
case "asc":
|
|
|
|
fallthrough
|
|
|
|
case "desc":
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return ErrInvalidSortDirection
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildChangelog(ctx *context.Context) ([]string, error) {
|
2021-10-04 14:32:30 +02:00
|
|
|
log, err := getChangelog(ctx, ctx.Git.CurrentTag)
|
2017-10-20 00:44:12 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-04-19 14:31:57 +02:00
|
|
|
entries := strings.Split(log, "\n")
|
2017-10-20 00:44:12 +02:00
|
|
|
entries = entries[0 : len(entries)-1]
|
|
|
|
entries, err = filterEntries(ctx, entries)
|
|
|
|
if err != nil {
|
|
|
|
return entries, err
|
|
|
|
}
|
|
|
|
return sortEntries(ctx, entries), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func filterEntries(ctx *context.Context, entries []string) ([]string, error) {
|
2017-10-16 00:40:53 +02:00
|
|
|
for _, filter := range ctx.Config.Changelog.Filters.Exclude {
|
2017-10-18 03:45:19 +02:00
|
|
|
r, err := regexp.Compile(filter)
|
|
|
|
if err != nil {
|
2017-10-20 00:44:12 +02:00
|
|
|
return entries, err
|
2017-10-18 03:45:19 +02:00
|
|
|
}
|
|
|
|
entries = remove(r, entries)
|
2017-10-16 00:40:53 +02:00
|
|
|
}
|
2017-10-20 00:44:12 +02:00
|
|
|
return entries, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func sortEntries(ctx *context.Context, entries []string) []string {
|
2021-04-19 14:31:57 +02:00
|
|
|
direction := ctx.Config.Changelog.Sort
|
2017-10-20 00:44:12 +02:00
|
|
|
if direction == "" {
|
|
|
|
return entries
|
|
|
|
}
|
2021-04-19 14:31:57 +02:00
|
|
|
result := make([]string, len(entries))
|
2017-10-20 00:44:12 +02:00
|
|
|
copy(result, entries)
|
|
|
|
sort.Slice(result, func(i, j int) bool {
|
2021-04-19 14:31:57 +02:00
|
|
|
imsg := extractCommitInfo(result[i])
|
|
|
|
jmsg := extractCommitInfo(result[j])
|
2017-10-20 00:44:12 +02:00
|
|
|
if direction == "asc" {
|
|
|
|
return strings.Compare(imsg, jmsg) < 0
|
|
|
|
}
|
|
|
|
return strings.Compare(imsg, jmsg) > 0
|
|
|
|
})
|
|
|
|
return result
|
2017-10-16 00:21:35 +02:00
|
|
|
}
|
|
|
|
|
2017-10-18 03:45:19 +02:00
|
|
|
func remove(filter *regexp.Regexp, entries []string) (result []string) {
|
2017-10-16 00:40:53 +02:00
|
|
|
for _, entry := range entries {
|
2019-10-09 21:07:51 +02:00
|
|
|
if !filter.MatchString(extractCommitInfo(entry)) {
|
2017-10-16 00:40:53 +02:00
|
|
|
result = append(result, entry)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2019-10-09 21:07:51 +02:00
|
|
|
func extractCommitInfo(line string) string {
|
|
|
|
return strings.Join(strings.Split(line, " ")[1:], " ")
|
2017-10-18 03:45:19 +02:00
|
|
|
}
|
|
|
|
|
2021-10-04 14:32:30 +02:00
|
|
|
func getChangelog(ctx *context.Context, tag string) (string, error) {
|
2017-10-16 00:21:35 +02:00
|
|
|
prev, err := previous(tag)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-10-04 14:32:30 +02:00
|
|
|
return doGetChangelog(ctx, prev, tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
func doGetChangelog(ctx *context.Context, prev, tag string) (string, error) {
|
|
|
|
l, err := getChangeloger(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2017-10-16 00:21:35 +02:00
|
|
|
}
|
2021-10-04 14:32:30 +02:00
|
|
|
return l.Log(ctx, prev, tag)
|
2017-10-16 00:21:35 +02:00
|
|
|
}
|
|
|
|
|
2021-10-04 14:32:30 +02:00
|
|
|
func getChangeloger(ctx *context.Context) (changeloger, error) {
|
|
|
|
switch ctx.Config.Changelog.Use {
|
|
|
|
case "git":
|
|
|
|
fallthrough
|
|
|
|
case "":
|
|
|
|
return gitChangeloger{}, nil
|
|
|
|
case "github":
|
2021-10-05 04:35:28 +02:00
|
|
|
return newGitHubChangeloger(ctx)
|
2021-10-04 14:32:30 +02:00
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("invalid changelog.use: %q", ctx.Config.Changelog.Use)
|
|
|
|
}
|
2017-10-16 00:21:35 +02:00
|
|
|
}
|
|
|
|
|
2021-10-05 04:35:28 +02:00
|
|
|
func newGitHubChangeloger(ctx *context.Context) (changeloger, error) {
|
|
|
|
cli, err := client.New(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
repo, err := git.ExtractRepoFromConfig()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &scmChangeloger{
|
|
|
|
client: cli,
|
|
|
|
repo: client.Repo{
|
|
|
|
Owner: repo.Owner,
|
|
|
|
Name: repo.Name,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2018-10-05 21:18:39 +02:00
|
|
|
func previous(tag string) (result string, err error) {
|
2020-01-31 20:38:56 +02:00
|
|
|
if tag := os.Getenv("GORELEASER_PREVIOUS_TAG"); tag != "" {
|
|
|
|
return tag, nil
|
|
|
|
}
|
|
|
|
|
2018-10-05 21:18:39 +02:00
|
|
|
result, err = git.Clean(git.Run("describe", "--tags", "--abbrev=0", fmt.Sprintf("tags/%s^", tag)))
|
2017-10-16 00:21:35 +02:00
|
|
|
if err != nil {
|
2018-10-05 21:18:39 +02:00
|
|
|
result, err = git.Clean(git.Run("rev-list", "--max-parents=0", "HEAD"))
|
2017-10-16 00:21:35 +02:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 02:07:47 +02:00
|
|
|
func loadContent(ctx *context.Context, fileName, tmplName string) (string, error) {
|
|
|
|
if tmplName != "" {
|
|
|
|
log.Debugf("loading template %s", tmplName)
|
|
|
|
content, err := loadFromFile(tmplName)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return tmpl.New(ctx).Apply(content)
|
|
|
|
}
|
|
|
|
|
|
|
|
if fileName != "" {
|
|
|
|
log.Debugf("loading file %s", fileName)
|
|
|
|
return loadFromFile(fileName)
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", nil
|
|
|
|
}
|
2021-10-04 14:32:30 +02:00
|
|
|
|
|
|
|
type changeloger interface {
|
|
|
|
Log(ctx *context.Context, prev, current string) (string, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type gitChangeloger struct{}
|
|
|
|
|
|
|
|
var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
|
|
|
|
|
|
|
|
func (g gitChangeloger) Log(ctx *context.Context, prev, current string) (string, error) {
|
|
|
|
args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
|
|
|
|
if validSHA1.MatchString(prev) {
|
|
|
|
args = append(args, prev, current)
|
|
|
|
} else {
|
|
|
|
args = append(args, fmt.Sprintf("tags/%s..tags/%s", prev, current))
|
|
|
|
}
|
|
|
|
return git.Run(args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
type scmChangeloger struct {
|
|
|
|
client client.Client
|
2021-10-05 04:35:28 +02:00
|
|
|
repo client.Repo
|
2021-10-04 14:32:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *scmChangeloger) Log(ctx *context.Context, prev, current string) (string, error) {
|
2021-10-05 04:35:28 +02:00
|
|
|
return c.client.Changelog(ctx, c.repo, prev, current)
|
2021-10-04 14:32:30 +02:00
|
|
|
}
|