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 }