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 }