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