Compare commits

..

15 Commits

Author SHA1 Message Date
maulik13
47a54436f5 feat(changelog): add a function in the funcMap to return commit URL 2021-02-23 14:25:09 +01:00
maulik13
deed3a630e docs(README): update available fields/objects for a changelog template 2021-02-23 14:25:09 +01:00
maulik13
df058a927f refactor(changelog): remove unused Version and Now fields, fixed spelling 2021-02-23 14:25:09 +01:00
maulik13
5a58d039fb refactor(angular): update default separator variable in angular 2021-02-23 14:25:09 +01:00
maulik13
08ab3af547 fix(analyzer): remove extra quote in structtag 2021-02-23 14:25:09 +01:00
maulik13
7208daed1f feat(angular): update angular to include new structured fields 2021-02-23 14:25:09 +01:00
maulik13
a20992af14 feat(conventional): parse body and footers according to the rules
Previous assumption about multiple labeled body blocks and footers is
not correct. There is only one body text block with multi-line support.
A footer always starts with a token with a separator.
- A body ends when a footer is found or text ends.
- A footer ends when another footer is found or text ends.
2021-02-23 14:25:09 +01:00
maulik13
dc4d1c581a feat(analyzer): update AnalyzedCommit to add flexibility in parsing a message
This provides flexibility of parsing and rendering structured messages
with more detail in the changelog and helps extract metadata from the 
message. The new structure can be used to split a message in multiple 
blocks (e.g. footer)
2021-02-23 14:25:09 +01:00
Sebastian
81bdb68ee4 Merge pull request #58 from maulik13/changelog-full-template
Changelog full template
2021-02-12 17:23:32 +01:00
maulik13
c485c3ee85 docs(changelog): update changelog template example 2021-02-12 13:55:08 +01:00
Sebastian
86c9512479 Update main.yml 2021-02-11 19:36:20 +01:00
Sebastian
4574d00c28 chore(ci): add pull request 2021-02-11 19:36:20 +01:00
Sebastian
0c4310d60b chore(ci): check if pr is fork 2021-02-11 19:36:20 +01:00
maulik13
3a37a5e1db feat(changelog): add string functions for changelog template 2021-02-08 11:30:06 +01:00
maulik13
9594f39caa feat(changelog): allow using of TemplatePath file for full changelog text 2021-02-08 11:15:27 +01:00
16 changed files with 630 additions and 89 deletions

View File

@@ -1,9 +1,9 @@
name: Go
on:
pull_request:
push:
branches:
- master
pull_request:
jobs:
build:
strategy:
@@ -21,11 +21,10 @@ jobs:
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Lint
run: |
export PATH=$PATH:$(go env GOPATH)/bin
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.35.2
golangci-lint run ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.29
- name: Run tests
run: go test ./...
@@ -41,7 +40,7 @@ jobs:
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o build/go-semantic-release.windows_x86_64.exe -ldflags "-w -s -X main.version=`./build/go-semantic-release-temp next`" ./cmd/go-semantic-release/
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o build/go-semantic-release.darwin_x86_64 -ldflags "-w -s -X main.version=`./build/go-semantic-release-temp next`" ./cmd/go-semantic-release/
- name: Build Docker image
if: matrix.go == '1.15'
if: matrix.go == '1.15' && github.repository == 'Nightapes/go-semantic-release'
run: |
docker login -u nightapes -p ${{ secrets.DOCKER_PASSWORD }}
docker login -u nightapes -p ${{ secrets.GITHUB_TOKEN }} docker.pkg.github.com
@@ -66,6 +65,7 @@ jobs:
name: build
path: build
- name: Release
if: github.repository == 'Nightapes/go-semantic-release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -73,3 +73,10 @@ jobs:
docker login -u nightapes -p ${{ secrets.DOCKER_PASSWORD }}
docker login -u nightapes -p $GITHUB_TOKEN docker.pkg.github.com
./build/go-semantic-release-temp release --loglevel trace
- name: Release fork
if: github.repository != 'Nightapes/go-semantic-release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
chmod -R +x build
./build/go-semantic-release-temp release --loglevel trace

View File

@@ -160,14 +160,58 @@ Hooks will run when calling `release`. Hooks run only if a release will be trigg
#### Changelog
Following variables can be used for templates:
* `Commits` string
* `Version` string
* `Now` time.Time
* `Backtick` string
* `HasDocker` bool
* `HasDockerLatest` bool
* `DockerRepository` string
Following variables and objects can be used for templates:
__Top level__
| Field | Type | Description |
| -------- | ------ | ----- |
| `Commits` | string | Fully rendered commit messages. This is left for backward compatibility. |
| `CommitsContent` | commitsContent | Raw parsed commit data. Use this if you want to customize the output. |
| `Version` | string | Next release version |
| `Now` | time.Time | Current time of generating changelog |
| `Backtick` | string | Backtick character |
| `HasDocker` | bool | If a docker repository is set in the config. |
| `HasDockerLatest` | bool | If `latest` image was uploaded |
| `DockerRepository` | string | Docker repository |
__commitsContent__
| Field | Type | Description |
| -------- | ------ | ----- |
| `Commits` | map[string][]AnalyzedCommit | Commits grouped by commit type |
| `BreakingChanges` | []AnalyzedCommit | Analyzed commit structure |
| `Order` | []string | Ordered list of types |
| `HasURL` | bool | If a URL is available for commits |
| `URL` | string | URL for to the commit with {{hash}} suffix |
__AnalyzedCommit__
| Field | Type | Description |
| -------- | ------ | ----- |
| `Commit` | Commit | Original GIT commit |
| `Tag` | string | Type of commit (e.g. feat, fix, ...) |
| `TagString` | string | Full name of the type |
| `Scope` | bool | Scope value from the commit |
| `Subject` | string | URL for to the commit with {{hash}} suffix |
| `MessageBlocks` | map[string][]MessageBlock | Different sections of a message (e.g. body, footer etc.) |
| `IsBreaking` | bool | If this commit contains a breaking change |
| `Print` | bool | Should this commit be included in Changelog output |
__Commit__
| Field | Type | Description |
| -------- | ------ | ----- |
| `Message` | string | Original git commit message |
| `Author` | string | Name of the author |
| `Hash` | string | Commit hash value "|
__MessageBlock__
| Field | Type | Description |
| -------- | ------ | ----- |
| `Label` | string | Label for a block (optional). This will usually be a token used in a footer |
| `Content` | string | The parsed content of a block |
```yml
changelog:

View File

@@ -1,5 +1,26 @@
{{ define "commitList" }}
{{ range $index,$commit := .BreakingChanges -}}
{{ if eq $index 0 -}}
## BREAKING CHANGES
{{ end -}}
* {{ if $commit.Scope }}**{{$.Backtick}}{{$commit.Scope}}{{$.Backtick}}**{{ end }} {{$commit.ParsedBreakingChangeMessage}}
introduced by commit:
{{$commit.ParsedMessage}} {{if $.HasURL}} ([{{ printf "%.7s" $commit.Commit.Hash}}]({{ replace $.URL "{{hash}}" $commit.Commit.Hash}})){{end}}
{{ end -}}
{{ range $key := .Order -}}
{{ $commits := index $.Commits $key -}}
{{ if $commits -}}
### {{ $key }}
{{ range $index,$commit := $commits -}}
* {{ if $commit.Scope }}**{{$.Backtick}}{{$commit.Scope}}{{$.Backtick}}** {{end}}{{$commit.ParsedMessage}}{{if $.HasURL}} ([{{ printf "%.7s" $commit.Commit.Hash}}]({{ replace $.URL "{{hash}}" $commit.Commit.Hash}})){{end}}
{{ end -}}
{{ end -}}
{{ end -}}
{{ end -}}
# My custom release template v{{$.Version}} ({{.Now.Format "2006-01-02"}})
{{ .Commits -}}
{{ template "commitList" .CommitsContent -}}
{{ if .HasDocker}}
## Docker image

View File

@@ -2,17 +2,26 @@
package analyzer
import (
"bufio"
"fmt"
"regexp"
"strings"
"github.com/Nightapes/go-semantic-release/internal/shared"
"github.com/Nightapes/go-semantic-release/pkg/config"
log "github.com/sirupsen/logrus"
)
const breakingChangeKeywords = "BREAKING CHANGE"
const defaultBreakingChangePrefix = breakingChangeKeywords + ":"
const footerTokenRegex = "^(?P<token>[^\\s][\\w\\- ]+[^\\s])<SEP>.*"
var defaultTokenSeparators = [2]string{ ": ", " #"}
// Analyzer struct
type Analyzer struct {
analyzeCommits analyzeCommits
Config config.ChangelogConfig
analyzeCommits analyzeCommits
ChangelogConfig config.ChangelogConfig
AnalyzerConfig config.AnalyzerConfig
}
// Rule for commits
@@ -24,14 +33,15 @@ type Rule struct {
}
type analyzeCommits interface {
analyze(commit shared.Commit, tag Rule) (*shared.AnalyzedCommit, bool)
analyze(commit shared.Commit, tag Rule) *shared.AnalyzedCommit
getRules() []Rule
}
// New Analyzer struct for given commit format
func New(format string, config config.ChangelogConfig) (*Analyzer, error) {
func New(format string, analyzerConfig config.AnalyzerConfig, chglogConfig config.ChangelogConfig) (*Analyzer, error) {
analyzer := &Analyzer{
Config: config,
AnalyzerConfig: analyzerConfig,
ChangelogConfig: chglogConfig,
}
switch format {
@@ -39,7 +49,7 @@ func New(format string, config config.ChangelogConfig) (*Analyzer, error) {
analyzer.analyzeCommits = newAngular()
log.Debugf("Commit format set to %s", ANGULAR)
case CONVENTIONAL:
analyzer.analyzeCommits = newConventional()
analyzer.analyzeCommits = newConventional(analyzerConfig)
log.Debugf("Commit format set to %s", CONVENTIONAL)
default:
return nil, fmt.Errorf("invalid commit format: %s", format)
@@ -62,14 +72,14 @@ func (a *Analyzer) Analyze(commits []shared.Commit) map[shared.Release][]shared.
for _, commit := range commits {
for _, rule := range a.analyzeCommits.getRules() {
analyzedCommit, hasBreakingChange := a.analyzeCommits.analyze(commit, rule)
analyzedCommit := a.analyzeCommits.analyze(commit, rule)
if analyzedCommit == nil {
continue
}
if a.Config.PrintAll || rule.Changelog {
if a.ChangelogConfig.PrintAll || rule.Changelog {
analyzedCommit.Print = true
}
if hasBreakingChange {
if analyzedCommit.IsBreaking {
analyzedCommits["major"] = append(analyzedCommits["major"], *analyzedCommit)
break
}
@@ -80,3 +90,114 @@ func (a *Analyzer) Analyze(commits []shared.Commit) map[shared.Release][]shared.
log.Debugf("Analyzed commits: major=%d minor=%d patch=%d none=%d", len(analyzedCommits["major"]), len(analyzedCommits["minor"]), len(analyzedCommits["patch"]), len(analyzedCommits["none"]))
return analyzedCommits
}
//
// getRegexMatchedMap will match a regex with named groups and map the matching
// results to corresponding group names
//
func getRegexMatchedMap(regEx, url string) (paramsMap map[string]string) {
var compRegEx = regexp.MustCompile(regEx)
match := compRegEx.FindStringSubmatch(url)
paramsMap = make(map[string]string)
for i, name := range compRegEx.SubexpNames() {
if i > 0 && i <= len(match) {
paramsMap[name] = match[i]
}
}
return paramsMap
}
//
// getMessageBlocksFromTexts converts strings to an array of MessageBlock
//
func getMessageBlocksFromTexts(txtArray, separators []string) []shared.MessageBlock {
blocks := make([]shared.MessageBlock, len(txtArray))
for i, line := range txtArray{
blocks[i] = parseMessageBlock(line, separators)
}
return blocks
}
//
// parseMessageBlock parses a text in to MessageBlock
//
func parseMessageBlock(msg string, separators []string) shared.MessageBlock {
msgBlock := shared.MessageBlock{
Label: "",
Content: msg,
}
if token, sep := findFooterToken(msg, separators); len(token) > 0{
msgBlock.Label = token
content := strings.Replace(msg, token + sep, "", 1)
msgBlock.Content = strings.TrimSpace(content)
}
return msgBlock
}
//
// findFooterToken checks if given text has a token with one of the separators and returns a token
//
func findFooterToken(text string, separators []string) (token string, sep string) {
for _, sep := range separators {
regex := strings.Replace(footerTokenRegex, "<SEP>", sep, 1)
matches := getRegexMatchedMap(regex, text)
if token, ok := matches["token"]; ok {
return token, sep
}
}
return "", ""
}
//
// getDefaultMessageBlockMap parses a text block and splits in to different sections.
// default logic to distinguish different parts is:
// - Body starts right after the header (without beginning with a token)
// - Body ends when a footer is discovered or text ends
// - A footer is detected when it starts with a token ending with a separator
// - A footer ends when another footer is found or text ends
//
func getDefaultMessageBlockMap(txtBlock string, tokenSep []string) map[string][]shared.MessageBlock{
msgBlockMap := make(map[string][]shared.MessageBlock)
footers := make([]string, 0)
body, footerBlock, line := "", "", ""
footerFound := false
// Look through each line
scanner := bufio.NewScanner(strings.NewReader(txtBlock))
for scanner.Scan() {
line = scanner.Text()
if token, _ := findFooterToken(line, tokenSep); len(token) > 0 {
// if footer was already found from before
if len(footerBlock) > 0{
footers = append(footers, strings.TrimSpace(footerBlock))
}
footerFound = true
footerBlock = ""
}
//'\n' is removed when reading from scanner
if !footerFound {
body += line + "\n"
}else{
footerBlock += line + "\n"
}
}
if len(footerBlock) > 0 {
footers = append(footers, strings.TrimSpace(footerBlock))
}
body = strings.TrimSpace(body)
if len(body) > 0{
msgBlockMap["body"] = []shared.MessageBlock {{
Label: "",
Content: body,
} }
}
footerBlocks := getMessageBlocksFromTexts(footers, tokenSep)
if len(footerBlocks) > 0 {
msgBlockMap["footer"] = footerBlocks
}
return msgBlockMap
}

View File

@@ -10,7 +10,7 @@ import (
func TestAnalyzer(t *testing.T) {
_, err := analyzer.New("unknown", config.ChangelogConfig{})
_, err := analyzer.New("unknown", config.AnalyzerConfig{}, config.ChangelogConfig{})
assert.Error(t, err)
}

View File

@@ -2,7 +2,7 @@
package analyzer
import (
"regexp"
"github.com/Nightapes/go-semantic-release/pkg/config"
"strings"
log "github.com/sirupsen/logrus"
@@ -14,14 +14,16 @@ type angular struct {
rules []Rule
regex string
log *log.Entry
config config.AnalyzerConfig
}
// ANGULAR identifier
const ANGULAR = "angular"
var angularFooterTokenSep = defaultTokenSeparators
func newAngular() *angular {
return &angular{
regex: `^(TAG)(?:\((.*)\))?: (?s)(.*)`,
regex: `^(?P<type>\w*)(?:\((?P<scope>.*)\))?: (?P<subject>.*)`,
log: log.WithField("analyzer", ANGULAR),
rules: []Rule{
{
@@ -86,37 +88,52 @@ func (a *angular) getRules() []Rule {
return a.rules
}
func (a *angular) analyze(commit shared.Commit, rule Rule) (*shared.AnalyzedCommit, bool) {
re := regexp.MustCompile(strings.Replace(a.regex, "TAG", rule.Tag, -1))
matches := re.FindStringSubmatch(commit.Message)
if matches == nil {
a.log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag)
return nil, false
func (a *angular) analyze(commit shared.Commit, rule Rule) *shared.AnalyzedCommit {
tokenSep := append(a.config.TokenSeparators, angularFooterTokenSep[:]...)
firstSplit := strings.SplitN(commit.Message, "\n", 2)
header := firstSplit[0]
body := ""
if len(firstSplit) > 1 {
body = firstSplit[1]
}
matches := getRegexMatchedMap(a.regex, header)
if len(matches) == 0 || matches["type"] != rule.Tag{
a.log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag)
return nil
}
msgBlockMap := getDefaultMessageBlockMap(body, tokenSep)
analyzed := &shared.AnalyzedCommit{
Commit: commit,
Tag: rule.Tag,
TagString: rule.TagString,
Scope: shared.Scope(matches[2]),
Scope: shared.Scope(matches["scope"]),
Subject: strings.TrimSpace(matches["subject"]),
MessageBlocks: msgBlockMap,
}
message := strings.Join(matches[3:], "")
if !strings.Contains(message, "BREAKING CHANGE:") {
analyzed.ParsedMessage = strings.Trim(message, " ")
isBreaking := strings.Contains(commit.Message, defaultBreakingChangePrefix)
analyzed.IsBreaking = isBreaking
oldFormatMessage := strings.TrimSpace(matches["subject"] + "\n" + body)
if !isBreaking {
analyzed.ParsedMessage = strings.Trim(oldFormatMessage, " ")
a.log.Tracef("%s: found %s", commit.Message, rule.Tag)
return analyzed, false
return analyzed
}
a.log.Tracef(" %s, BREAKING CHANGE found", commit.Message)
breakingChange := strings.SplitN(message, "BREAKING CHANGE:", 2)
breakingChange := strings.SplitN(oldFormatMessage, defaultBreakingChangePrefix, 2)
if len(breakingChange) > 1 {
analyzed.ParsedMessage = strings.TrimSpace(breakingChange[0])
analyzed.ParsedBreakingChangeMessage = strings.TrimSpace(breakingChange[1])
return analyzed, true
} else {
analyzed.ParsedBreakingChangeMessage = breakingChange[0]
}
analyzed.ParsedBreakingChangeMessage = breakingChange[0]
return analyzed, true
return analyzed
}

View File

@@ -31,6 +31,8 @@ func TestAngular(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"major": {},
@@ -60,6 +62,8 @@ func TestAngular(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"major": {
@@ -75,6 +79,9 @@ func TestAngular(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "change api to v2",
IsBreaking: true,
Subject: "my first break BREAKING CHANGE: change api to v2",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"patch": {},
@@ -120,6 +127,8 @@ func TestAngular(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"major": {
@@ -135,6 +144,16 @@ func TestAngular(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "change api to v2",
IsBreaking: true,
Subject: "my first break",
MessageBlocks: map[string][]shared.MessageBlock{
"footer": {
shared.MessageBlock{
Label: "BREAKING CHANGE",
Content: "change api to v2",
},
},
},
},
},
"patch": {},
@@ -177,6 +196,8 @@ func TestAngular(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"none": {
@@ -192,6 +213,8 @@ func TestAngular(t *testing.T) {
TagString: "Changes to CI/CD",
Print: false,
ParsedBreakingChangeMessage: "",
Subject: "my first build",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"patch": {},
@@ -212,7 +235,7 @@ func TestAngular(t *testing.T) {
},
}
angular, err := analyzer.New("angular", config.ChangelogConfig{})
angular, err := analyzer.New("angular", config.AnalyzerConfig{}, config.ChangelogConfig{})
assert.NoError(t, err)
for _, test := range testConfigs {

View File

@@ -2,7 +2,7 @@
package analyzer
import (
"regexp"
"github.com/Nightapes/go-semantic-release/pkg/config"
"strings"
log "github.com/sirupsen/logrus"
@@ -14,14 +14,17 @@ type conventional struct {
rules []Rule
regex string
log *log.Entry
config config.AnalyzerConfig
}
// CONVENTIONAL identifier
const CONVENTIONAL = "conventional"
var conventionalFooterTokenSep = defaultTokenSeparators
func newConventional() *conventional {
func newConventional(config config.AnalyzerConfig) *conventional {
return &conventional{
regex: `^(TAG)(?:\((.*)\))?(\!)?: (?s)(.*)`,
config: config,
regex: `^(?P<type>\w*)(?:\((?P<scope>.*)\))?(?P<breaking>\!)?: (?P<subject>.*)`,
log: log.WithField("analyzer", CONVENTIONAL),
rules: []Rule{
{
@@ -86,37 +89,55 @@ func (a *conventional) getRules() []Rule {
return a.rules
}
func (a *conventional) analyze(commit shared.Commit, rule Rule) (*shared.AnalyzedCommit, bool) {
re := regexp.MustCompile(strings.Replace(a.regex, "TAG", rule.Tag, -1))
matches := re.FindStringSubmatch(commit.Message)
if matches == nil {
a.log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag)
return nil, false
func (a *conventional) analyze(commit shared.Commit, rule Rule) *shared.AnalyzedCommit {
tokenSep := append(a.config.TokenSeparators, conventionalFooterTokenSep[:]...)
firstSplit := strings.SplitN(commit.Message, "\n", 2)
header := firstSplit[0]
body := ""
if len(firstSplit) > 1 {
body = firstSplit[1]
}
matches := getRegexMatchedMap(a.regex, header)
if len(matches) == 0 || matches["type"] != rule.Tag{
a.log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag)
return nil
}
msgBlockMap := getDefaultMessageBlockMap(body, tokenSep)
analyzed := &shared.AnalyzedCommit{
Commit: commit,
Tag: rule.Tag,
TagString: rule.TagString,
Scope: shared.Scope(matches[2]),
Commit: commit,
Tag: rule.Tag,
TagString: rule.TagString,
Scope: shared.Scope(matches["scope"]),
Subject: strings.TrimSpace(matches["subject"]),
MessageBlocks: msgBlockMap,
}
message := strings.Join(matches[4:], "")
if matches[3] == "" && !strings.Contains(message, "BREAKING CHANGE:") {
analyzed.ParsedMessage = strings.Trim(message, " ")
isBreaking := matches["breaking"] == "!" || strings.Contains(commit.Message, defaultBreakingChangePrefix)
analyzed.IsBreaking = isBreaking
oldFormatMessage := strings.TrimSpace(matches["subject"] + "\n" + body)
if !isBreaking {
analyzed.ParsedMessage = strings.Trim(oldFormatMessage, " ")
a.log.Tracef("%s: found %s", commit.Message, rule.Tag)
return analyzed, false
return analyzed
}
a.log.Infof(" %s, BREAKING CHANGE found", commit.Message)
breakingChange := strings.SplitN(message, "BREAKING CHANGE:", 2)
breakingChange := strings.SplitN(oldFormatMessage, defaultBreakingChangePrefix, 2)
if len(breakingChange) > 1 {
analyzed.ParsedMessage = strings.TrimSpace(breakingChange[0])
analyzed.ParsedBreakingChangeMessage = strings.TrimSpace(breakingChange[1])
return analyzed, true
} else {
analyzed.ParsedBreakingChangeMessage = breakingChange[0]
}
analyzed.ParsedBreakingChangeMessage = breakingChange[0]
return analyzed, true
return analyzed
}

View File

@@ -30,6 +30,8 @@ func TestConventional(t *testing.T) {
ParsedMessage: "my first commit",
Tag: "feat",
TagString: "Features",
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
Print: true,
},
{
@@ -42,6 +44,8 @@ func TestConventional(t *testing.T) {
ParsedMessage: "no scope",
Tag: "feat",
TagString: "Features",
Subject: "no scope",
MessageBlocks: map[string][]shared.MessageBlock{},
Print: true,
},
},
@@ -77,6 +81,8 @@ func TestConventional(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"major": {
@@ -92,6 +98,9 @@ func TestConventional(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "my first break",
IsBreaking: true,
Subject: "my first break",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"patch": {},
@@ -125,6 +134,8 @@ func TestConventional(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"major": {
@@ -140,6 +151,15 @@ func TestConventional(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "change api to v2",
IsBreaking: true,
Subject: "my first break",
MessageBlocks: map[string][]shared.MessageBlock{
"footer" : { shared.MessageBlock{
Label: "BREAKING CHANGE",
Content: "change api to v2",
},
},
},
},
{
Commit: shared.Commit{
@@ -153,6 +173,15 @@ func TestConventional(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "hey from the change",
IsBreaking: true,
Subject: "my first break",
MessageBlocks: map[string][]shared.MessageBlock{
"footer" : {shared.MessageBlock{
Label: "BREAKING CHANGE",
Content: "hey from the change",
},
},
},
},
},
"patch": {},
@@ -212,6 +241,8 @@ func TestConventional(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"none": {
@@ -227,6 +258,8 @@ func TestConventional(t *testing.T) {
TagString: "Changes to CI/CD",
Print: false,
ParsedBreakingChangeMessage: "",
Subject: "my first build",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"patch": {},
@@ -262,6 +295,8 @@ func TestConventional(t *testing.T) {
TagString: "Changes to CI/CD",
Print: false,
ParsedBreakingChangeMessage: "",
Subject: "my first build",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
"patch": {{
@@ -275,6 +310,8 @@ func TestConventional(t *testing.T) {
Tag: "fix",
TagString: "Bug fixes",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
}},
"major": {},
},
@@ -293,7 +330,7 @@ func TestConventional(t *testing.T) {
},
}
conventional, err := analyzer.New("conventional", config.ChangelogConfig{})
conventional, err := analyzer.New("conventional", config.AnalyzerConfig{}, config.ChangelogConfig{})
assert.NoError(t, err)
for _, test := range testConfigs {
@@ -304,3 +341,156 @@ func TestConventional(t *testing.T) {
assert.Equalf(t, test.wantAnalyzedCommits["none"], analyzedCommits["none"], "Testcase %s should have none commits", test.testCase)
}
}
func TestConventional_BodyAndFooters(t *testing.T) {
t.Parallel()
testConfigs := []struct {
testCase string
commits []shared.Commit
expectedAnalyzedCommits map[shared.Release][]shared.AnalyzedCommit
}{
{
testCase: "Only body, no footer",
commits: []shared.Commit{
{
Message: "fix: squash bug for logging\n\nNow the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout",
Author: "me",
Hash: "12345667",
},
},
expectedAnalyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
"patch": {
{
Commit: shared.Commit{
Message: "fix: squash bug for logging\n\nNow the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout",
Author: "me",
Hash: "12345667",
},
Scope: "",
ParsedMessage: "squash bug for logging\n\nNow the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout",
Tag: "fix",
TagString: "Bug fixes",
Print: true,
Subject: "squash bug for logging",
MessageBlocks: map[string][]shared.MessageBlock{
"body": {
shared.MessageBlock{
Label: "",
Content: "Now the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout",
},
},
},
},
},
"major": {},
"minor": {},
"none": {},
},
},
{
testCase: "Only footers, no body",
commits: []shared.Commit{
{
Message: "fix: squash bug for logging\n\nNote: now the logs will not print lines twice.\n\nIssue: #123\nSeverity: medium",
Author: "me",
Hash: "12345667",
},
},
expectedAnalyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
"patch": {
{
Commit: shared.Commit{
Message: "fix: squash bug for logging\n\nNote: now the logs will not print lines twice.\n\nIssue: #123\nSeverity: medium",
Author: "me",
Hash: "12345667",
},
Scope: "",
ParsedMessage: "squash bug for logging\n\nNote: now the logs will not print lines twice.\n\nIssue: #123\nSeverity: medium",
Tag: "fix",
TagString: "Bug fixes",
Print: true,
Subject: "squash bug for logging",
MessageBlocks: map[string][]shared.MessageBlock{
"footer": {
shared.MessageBlock{
Label: "Note",
Content: "now the logs will not print lines twice.",
},
shared.MessageBlock{
Label: "Issue",
Content: "#123",
},
shared.MessageBlock{
Label: "Severity",
Content: "medium",
},
},
},
},
},
"major": {},
"minor": {},
"none": {},
},
},
{
testCase: "Body and footers",
commits: []shared.Commit{
{
Message: "fix: squash bug for logging\n\nNow the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout\n\nIssue: #123\nSeverity: medium",
Author: "me",
Hash: "12345667",
},
},
expectedAnalyzedCommits: map[shared.Release][]shared.AnalyzedCommit{
"patch": {
{
Commit: shared.Commit{
Message: "fix: squash bug for logging\n\nNow the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout\n\nIssue: #123\nSeverity: medium",
Author: "me",
Hash: "12345667",
},
Scope: "",
ParsedMessage: "squash bug for logging\n\nNow the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout\n\nIssue: #123\nSeverity: medium",
Tag: "fix",
TagString: "Bug fixes",
Print: true,
Subject: "squash bug for logging",
MessageBlocks: map[string][]shared.MessageBlock{
"body": {
shared.MessageBlock{
Label: "",
Content: "Now the logs will not print lines twice. The following changed:\n\n-Buffer -Stdout",
},
},
"footer": {
shared.MessageBlock{
Label: "Issue",
Content: "#123",
},
shared.MessageBlock{
Label: "Severity",
Content: "medium",
},
},
},
},
},
"major": {},
"minor": {},
"none": {},
},
},
}
conventional, err := analyzer.New("conventional", config.AnalyzerConfig{}, config.ChangelogConfig{})
assert.NoError(t, err)
for _, test := range testConfigs {
analyzedCommits := conventional.Analyze(test.commits)
assert.Equalf(t, test.expectedAnalyzedCommits["major"], analyzedCommits["major"], "Testcase %s should have major commits", test.testCase)
assert.Equalf(t, test.expectedAnalyzedCommits["minor"], analyzedCommits["minor"], "Testcase %s should have minor commits", test.testCase)
assert.Equalf(t, test.expectedAnalyzedCommits["patch"], analyzedCommits["patch"], "Testcase %s should have patch commits", test.testCase)
assert.Equalf(t, test.expectedAnalyzedCommits["none"], analyzedCommits["none"], "Testcase %s should have none commits", test.testCase)
}
}

View File

@@ -69,6 +69,8 @@ func TestWriteAndReadCache(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "add gitlab as release option",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
},

View File

@@ -1,6 +1,7 @@
package changelog
import (
"bufio"
"bytes"
"io/ioutil"
"strings"
@@ -31,9 +32,11 @@ introduced by commit:
{{ end -}}
{{ end -}}
{{ end -}}`
const defaultCommitListSubTemplate string = `{{ define "commitList" }}` + defaultCommitList + "{{ end }}"
const defaultChangelogTitle string = `v{{.Version}} ({{.Now.Format "2006-01-02"}})`
const defaultChangelog string = `# v{{$.Version}} ({{.Now.Format "2006-01-02"}})
{{ .Commits -}}
{{ template "commitList" .CommitsContent -}}
{{ if .HasDocker}}
## Docker image
@@ -51,7 +54,8 @@ or
`
type changelogContent struct {
Commits string
Commits string
CommitsContent commitsContent
Version string
Now time.Time
Backtick string
@@ -64,8 +68,6 @@ type commitsContent struct {
Commits map[string][]shared.AnalyzedCommit
BreakingChanges []shared.AnalyzedCommit
Order []string
Version string
Now time.Time
Backtick string
HasURL bool
URL string
@@ -89,11 +91,11 @@ func New(config *config.ReleaseConfig, rules []analyzer.Rule, releaseTime time.T
}
}
// GenerateChanglog from given commits
func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConfig, analyzedCommits map[shared.Release][]shared.AnalyzedCommit) (*shared.GeneratedChangelog, error) {
// GenerateChangelog from given commits
func (c *Changelog) GenerateChangelog(templateConfig shared.ChangelogTemplateConfig, analyzedCommits map[shared.Release][]shared.AnalyzedCommit) (*shared.GeneratedChangelog, error) {
commitsPerScope := map[string][]shared.AnalyzedCommit{}
commitsBreakingChange := []shared.AnalyzedCommit{}
var commitsBreakingChange []shared.AnalyzedCommit
order := make([]string, 0)
for _, rule := range c.rules {
@@ -106,7 +108,7 @@ func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConf
for _, commits := range analyzedCommits {
for _, commit := range commits {
if commit.Print {
if commit.ParsedBreakingChangeMessage != "" {
if commit.IsBreaking {
commitsBreakingChange = append(commitsBreakingChange, commit)
continue
}
@@ -119,9 +121,7 @@ func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConf
}
commitsContent := commitsContent{
Version: templateConfig.Version,
Commits: commitsPerScope,
Now: c.releaseTime,
BreakingChanges: commitsBreakingChange,
Backtick: "`",
Order: order,
@@ -130,6 +130,7 @@ func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConf
}
changelogContent := changelogContent{
CommitsContent: commitsContent,
Version: templateConfig.Version,
Now: c.releaseTime,
Backtick: "`",
@@ -137,13 +138,14 @@ func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConf
HasDockerLatest: c.config.Changelog.Docker.Latest,
DockerRepository: c.config.Changelog.Docker.Repository,
}
template := defaultChangelog
chglogTemplate := defaultCommitListSubTemplate + defaultChangelog
if c.config.Changelog.TemplatePath != "" {
content, err := ioutil.ReadFile(c.config.Changelog.TemplatePath)
if err != nil {
return nil, err
}
template = string(content)
chglogTemplate = string(content)
}
templateTitle := defaultChangelogTitle
@@ -152,30 +154,41 @@ func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConf
}
log.Debugf("Render title")
renderedTitle, err := generateTemplate(templateTitle, changelogContent)
renderedTitle, err := generateTemplate(templateTitle, changelogContent, nil)
if err != nil {
return nil, err
}
log.Debugf("Render commits")
renderedCommitList, err := generateTemplate(defaultCommitList, commitsContent)
renderedCommitList, err := generateTemplate(defaultCommitList, commitsContent, nil)
if err != nil {
return nil, err
}
log.Tracef("Commits %s", renderedCommitList)
changelogContent.Commits = renderedCommitList
extraFuncMap := template.FuncMap {
"commitUrl": func() string {return templateConfig.CommitURL},
}
log.Debugf("Render changelog")
renderedContent, err := generateTemplate(template, changelogContent)
renderedContent, err := generateTemplate(chglogTemplate, changelogContent, extraFuncMap)
return &shared.GeneratedChangelog{Title: renderedTitle, Content: renderedContent}, err
}
func generateTemplate(text string, values interface{}) (string, error) {
func generateTemplate(text string, values interface{}, extraFuncMap template.FuncMap) (string, error) {
funcMap := template.FuncMap{
"replace": replace,
"lower": lower,
"upper": upper,
"capitalize": capitalize,
"addPrefixToLines": addPrefixToLines,
}
for k, v := range extraFuncMap {
funcMap[k] = v
}
var tpl bytes.Buffer
@@ -193,3 +206,30 @@ func generateTemplate(text string, values interface{}) (string, error) {
func replace(input, from, to string) string {
return strings.Replace(input, from, to, -1)
}
func lower(input string) string {
return strings.ToLower(input)
}
func upper(input string) string {
return strings.ToUpper(input)
}
func capitalize(input string) string {
if len(input) > 0 {
return strings.ToUpper(string(input[0])) + input[1:]
}
return ""
}
// Adds a prefix to each line of the given text block
// this can be helpful in rendering correct indentation or bullets for multi-line texts
func addPrefixToLines(input, prefix string) string {
output := ""
scanner := bufio.NewScanner(strings.NewReader(input))
for scanner.Scan() {
output += prefix + scanner.Text() + "\n"
}
output = strings.TrimRight(output, "\n")
return output
}

View File

@@ -41,6 +41,8 @@ func TestChangelog(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
},
@@ -64,6 +66,8 @@ func TestChangelog(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
},
@@ -88,6 +92,8 @@ func TestChangelog(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
{
Commit: shared.Commit{
@@ -101,6 +107,15 @@ func TestChangelog(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "change api to v2",
IsBreaking: true,
Subject: "my first break",
MessageBlocks: map[string][]shared.MessageBlock{
"body" : { shared.MessageBlock{
Label: "BREAKING CHANGE",
Content: "change api to v2",
},
},
},
},
},
},
@@ -126,6 +141,15 @@ func TestChangelog(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "hey from the change",
IsBreaking: true,
Subject: "my first break",
MessageBlocks: map[string][]shared.MessageBlock{
"body" : { shared.MessageBlock{
Label: "BREAKING CHANGE",
Content: "hey from the change",
},
},
},
},
{
Commit: shared.Commit{
@@ -138,6 +162,8 @@ func TestChangelog(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my first commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
{
Commit: shared.Commit{
@@ -150,10 +176,12 @@ func TestChangelog(t *testing.T) {
Tag: "feat",
TagString: "Features",
Print: true,
Subject: "my second commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
{
Commit: shared.Commit{
Message: "feat: my new commit \n\nmy first break: BREAKING CHANGE: change api to v2",
Message: "feat: my new commit \n\nBREAKING CHANGE: change api to v2",
Author: "me",
Hash: "12345668",
},
@@ -163,6 +191,14 @@ func TestChangelog(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "change api to v2",
IsBreaking: true,
Subject: "my new commit",
MessageBlocks: map[string][]shared.MessageBlock{
"body": { shared.MessageBlock{
Label: "BREAKING CHANGE",
Content: "change api to v2",
}},
},
},
{
Commit: shared.Commit{
@@ -176,6 +212,9 @@ func TestChangelog(t *testing.T) {
TagString: "Features",
Print: true,
ParsedBreakingChangeMessage: "my next commit",
IsBreaking: true,
Subject: "my next commit",
MessageBlocks: map[string][]shared.MessageBlock{},
},
},
},
@@ -210,7 +249,7 @@ func TestChangelog(t *testing.T) {
for _, config := range testConfigs {
t.Run(config.testCase, func(t *testing.T) {
generatedChangelog, err := cl.GenerateChanglog(templateConfig, config.analyzedCommits)
generatedChangelog, err := cl.GenerateChangelog(templateConfig, config.analyzedCommits)
assert.Equalf(t, config.hasError, err != nil, "Testcase %s should have error: %t -> %s", config.testCase, config.hasError, err)
assert.Equalf(t, config.result, generatedChangelog, "Testcase %s should have generated changelog", config.testCase)
})

View File

@@ -37,13 +37,22 @@ type ChangelogTemplateConfig struct {
type AnalyzedCommit struct {
Commit Commit `yaml:"commit"`
ParsedMessage string `yaml:"parsedMessage"`
Scope Scope `yaml:"scope"`
ParsedBreakingChangeMessage string `yaml:"parsedBreakingChangeMessage"`
Tag string `yaml:"tag"`
TagString string `yaml:"tagString"`
Scope Scope `yaml:"scope"`
Subject string `yaml:"subject"`
MessageBlocks map[string][]MessageBlock `yaml:"messageBlocks"`
IsBreaking bool `yaml:"isBreaking"`
Print bool `yaml:"print"`
}
// MessageBlock represents a block in the body section of a commit message
type MessageBlock struct {
Label string `yaml:"label"`
Content string `yaml:"content"`
}
//Scope of the commit, like feat, fix,..
type Scope string

View File

@@ -13,6 +13,11 @@ const (
DefaultTagPrefix = "v"
)
// AnalyzerConfig struct
type AnalyzerConfig struct {
TokenSeparators []string `yaml:"tokenSeparators"`
}
// ChangelogConfig struct
type ChangelogConfig struct {
PrintAll bool `yaml:"printAll,omitempty"`
@@ -83,6 +88,7 @@ type Checksum struct {
type ReleaseConfig struct {
CommitFormat string `yaml:"commitFormat"`
Branch map[string]string `yaml:"branch"`
Analyzer AnalyzerConfig `yaml:"analyzer"`
Changelog ChangelogConfig `yaml:"changelog,omitempty"`
Release string `yaml:"release,omitempty"`
GitHubProvider GitHubProvider `yaml:"github,omitempty"`

View File

@@ -100,6 +100,7 @@ github:
Compress: false}},
ReleaseTitle: "go-semantic-release release",
IsPreRelease: false,
Analyzer: config.AnalyzerConfig{TokenSeparators: []string{}},
}, result)
}

View File

@@ -39,7 +39,7 @@ func New(c *config.ReleaseConfig, repository string, checkConfig bool) (*Semanti
return nil, err
}
analyzer, err := analyzer.New(c.CommitFormat, c.Changelog)
analyzer, err := analyzer.New(c.CommitFormat, c.Analyzer, c.Changelog)
if err != nil {
return nil, err
}
@@ -180,7 +180,7 @@ func (s *SemanticRelease) SetVersion(provider *ci.ProviderConfig, version string
// GetChangelog from last version till now
func (s *SemanticRelease) GetChangelog(releaseVersion *shared.ReleaseVersion) (*shared.GeneratedChangelog, error) {
c := changelog.New(s.config, s.analyzer.GetRules(), time.Now())
return c.GenerateChanglog(shared.ChangelogTemplateConfig{
return c.GenerateChangelog(shared.ChangelogTemplateConfig{
Version: releaseVersion.Next.Version.String(),
Hash: releaseVersion.Last.Commit,
CommitURL: s.releaser.GetCommitURL(),