github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/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/apex/log" 15 "github.com/goreleaser/goreleaser/internal/git" 16 "github.com/goreleaser/goreleaser/internal/pipe" 17 "github.com/goreleaser/goreleaser/internal/tmpl" 18 "github.com/goreleaser/goreleaser/pkg/context" 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 if err != nil { 147 return entries, err 148 } 149 return sortEntries(ctx, entries), nil 150 } 151 152 func filterEntries(ctx *context.Context, entries []string) ([]string, error) { 153 for _, filter := range ctx.Config.Changelog.Filters.Exclude { 154 r, err := regexp.Compile(filter) 155 if err != nil { 156 return entries, err 157 } 158 entries = remove(r, entries) 159 } 160 return entries, nil 161 } 162 163 func sortEntries(ctx *context.Context, entries []string) []string { 164 var direction = ctx.Config.Changelog.Sort 165 if direction == "" { 166 return entries 167 } 168 var result = make([]string, len(entries)) 169 copy(result, entries) 170 sort.Slice(result, func(i, j int) bool { 171 var imsg = extractCommitInfo(result[i]) 172 var jmsg = extractCommitInfo(result[j]) 173 if direction == "asc" { 174 return strings.Compare(imsg, jmsg) < 0 175 } 176 return strings.Compare(imsg, jmsg) > 0 177 }) 178 return result 179 } 180 181 func remove(filter *regexp.Regexp, entries []string) (result []string) { 182 for _, entry := range entries { 183 if !filter.MatchString(extractCommitInfo(entry)) { 184 result = append(result, entry) 185 } 186 } 187 return result 188 } 189 190 func extractCommitInfo(line string) string { 191 return strings.Join(strings.Split(line, " ")[1:], " ") 192 } 193 194 func getChangelog(tag string) (string, error) { 195 prev, err := previous(tag) 196 if err != nil { 197 return "", err 198 } 199 if isSHA1(prev) { 200 return gitLog(prev, tag) 201 } 202 return gitLog(fmt.Sprintf("tags/%s..tags/%s", prev, tag)) 203 } 204 205 func gitLog(refs ...string) (string, error) { 206 var args = []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"} 207 args = append(args, refs...) 208 return git.Run(args...) 209 } 210 211 func previous(tag string) (result string, err error) { 212 if tag := os.Getenv("GORELEASER_PREVIOUS_TAG"); tag != "" { 213 return tag, nil 214 } 215 216 result, err = git.Clean(git.Run("describe", "--tags", "--abbrev=0", fmt.Sprintf("tags/%s^", tag))) 217 if err != nil { 218 result, err = git.Clean(git.Run("rev-list", "--max-parents=0", "HEAD")) 219 } 220 return 221 } 222 223 var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`) 224 225 // isSHA1 te lets us know if the ref is a SHA1 or not. 226 func isSHA1(ref string) bool { 227 return validSHA1.MatchString(ref) 228 }