github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/internal/pipe/changelog/changelog.go (about)

     1  // Package changelog provides the release changelog to goreleaser.
     2  package changelog
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/apex/log"
    14  	"github.com/goreleaser/goreleaser/internal/git"
    15  	"github.com/goreleaser/goreleaser/internal/tmpl"
    16  	"github.com/goreleaser/goreleaser/pkg/context"
    17  )
    18  
    19  // ErrInvalidSortDirection happens when the sort order is invalid.
    20  var ErrInvalidSortDirection = errors.New("invalid sort direction")
    21  
    22  // Pipe for checksums.
    23  type Pipe struct{}
    24  
    25  func (Pipe) String() string                 { return "generating changelog" }
    26  func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Changelog.Skip || ctx.Snapshot }
    27  
    28  // Run the pipe.
    29  func (Pipe) Run(ctx *context.Context) error {
    30  	notes, err := loadContent(ctx, ctx.ReleaseNotesFile, ctx.ReleaseNotesTmpl)
    31  	if err != nil {
    32  		return err
    33  	}
    34  	ctx.ReleaseNotes = notes
    35  
    36  	if ctx.ReleaseNotes != "" {
    37  		return nil
    38  	}
    39  
    40  	footer, err := loadContent(ctx, ctx.ReleaseFooterFile, ctx.ReleaseFooterTmpl)
    41  	if err != nil {
    42  		return err
    43  	}
    44  
    45  	header, err := loadContent(ctx, ctx.ReleaseHeaderFile, ctx.ReleaseHeaderTmpl)
    46  	if err != nil {
    47  		return err
    48  	}
    49  
    50  	if err := checkSortDirection(ctx.Config.Changelog.Sort); err != nil {
    51  		return err
    52  	}
    53  
    54  	entries, err := buildChangelog(ctx)
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	changelogStringJoiner := "\n"
    60  	if ctx.TokenType == context.TokenTypeGitLab || ctx.TokenType == context.TokenTypeGitea {
    61  		// We need two or more whitespace to let markdown interpret
    62  		// it as newline. See https://docs.gitlab.com/ee/user/markdown.html#newlines for details
    63  		log.Debug("is gitlab or gitea changelog")
    64  		changelogStringJoiner = "   \n"
    65  	}
    66  
    67  	changelogElements := []string{
    68  		"## Changelog",
    69  		strings.Join(entries, changelogStringJoiner),
    70  	}
    71  	if header != "" {
    72  		changelogElements = append([]string{header}, changelogElements...)
    73  	}
    74  	if footer != "" {
    75  		changelogElements = append(changelogElements, footer)
    76  	}
    77  
    78  	ctx.ReleaseNotes = strings.Join(changelogElements, "\n\n")
    79  	if !strings.HasSuffix(ctx.ReleaseNotes, "\n") {
    80  		ctx.ReleaseNotes += "\n"
    81  	}
    82  
    83  	path := filepath.Join(ctx.Config.Dist, "CHANGELOG.md")
    84  	log.WithField("changelog", path).Info("writing")
    85  	return os.WriteFile(path, []byte(ctx.ReleaseNotes), 0o644) //nolint: gosec
    86  }
    87  
    88  func loadFromFile(file string) (string, error) {
    89  	bts, err := os.ReadFile(file)
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  	return string(bts), nil
    94  }
    95  
    96  func checkSortDirection(mode string) error {
    97  	switch mode {
    98  	case "":
    99  		fallthrough
   100  	case "asc":
   101  		fallthrough
   102  	case "desc":
   103  		return nil
   104  	}
   105  	return ErrInvalidSortDirection
   106  }
   107  
   108  func buildChangelog(ctx *context.Context) ([]string, error) {
   109  	log, err := getChangelog(ctx.Git.CurrentTag)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	entries := strings.Split(log, "\n")
   114  	entries = entries[0 : len(entries)-1]
   115  	entries, err = filterEntries(ctx, entries)
   116  	if err != nil {
   117  		return entries, err
   118  	}
   119  	return sortEntries(ctx, entries), nil
   120  }
   121  
   122  func filterEntries(ctx *context.Context, entries []string) ([]string, error) {
   123  	for _, filter := range ctx.Config.Changelog.Filters.Exclude {
   124  		r, err := regexp.Compile(filter)
   125  		if err != nil {
   126  			return entries, err
   127  		}
   128  		entries = remove(r, entries)
   129  	}
   130  	return entries, nil
   131  }
   132  
   133  func sortEntries(ctx *context.Context, entries []string) []string {
   134  	direction := ctx.Config.Changelog.Sort
   135  	if direction == "" {
   136  		return entries
   137  	}
   138  	result := make([]string, len(entries))
   139  	copy(result, entries)
   140  	sort.Slice(result, func(i, j int) bool {
   141  		imsg := extractCommitInfo(result[i])
   142  		jmsg := extractCommitInfo(result[j])
   143  		if direction == "asc" {
   144  			return strings.Compare(imsg, jmsg) < 0
   145  		}
   146  		return strings.Compare(imsg, jmsg) > 0
   147  	})
   148  	return result
   149  }
   150  
   151  func remove(filter *regexp.Regexp, entries []string) (result []string) {
   152  	for _, entry := range entries {
   153  		if !filter.MatchString(extractCommitInfo(entry)) {
   154  			result = append(result, entry)
   155  		}
   156  	}
   157  	return result
   158  }
   159  
   160  func extractCommitInfo(line string) string {
   161  	return strings.Join(strings.Split(line, " ")[1:], " ")
   162  }
   163  
   164  func getChangelog(tag string) (string, error) {
   165  	prev, err := previous(tag)
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	if isSHA1(prev) {
   170  		return gitLog(prev, tag)
   171  	}
   172  	return gitLog(fmt.Sprintf("tags/%s..tags/%s", prev, tag))
   173  }
   174  
   175  func gitLog(refs ...string) (string, error) {
   176  	args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
   177  	args = append(args, refs...)
   178  	return git.Run(args...)
   179  }
   180  
   181  func previous(tag string) (result string, err error) {
   182  	if tag := os.Getenv("GORELEASER_PREVIOUS_TAG"); tag != "" {
   183  		return tag, nil
   184  	}
   185  
   186  	result, err = git.Clean(git.Run("describe", "--tags", "--abbrev=0", fmt.Sprintf("tags/%s^", tag)))
   187  	if err != nil {
   188  		result, err = git.Clean(git.Run("rev-list", "--max-parents=0", "HEAD"))
   189  	}
   190  	return
   191  }
   192  
   193  var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
   194  
   195  // isSHA1 te lets us know if the ref is a SHA1 or not.
   196  func isSHA1(ref string) bool {
   197  	return validSHA1.MatchString(ref)
   198  }
   199  
   200  func loadContent(ctx *context.Context, fileName, tmplName string) (string, error) {
   201  	if tmplName != "" {
   202  		log.Debugf("loading template %s", tmplName)
   203  		content, err := loadFromFile(tmplName)
   204  		if err != nil {
   205  			return "", err
   206  		}
   207  		return tmpl.New(ctx).Apply(content)
   208  	}
   209  
   210  	if fileName != "" {
   211  		log.Debugf("loading file %s", fileName)
   212  		return loadFromFile(fileName)
   213  	}
   214  
   215  	return "", nil
   216  }