Skip to content

Commit

Permalink
feat(conventional): parse body and footers according to the rules
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
maulik13 authored and fwiedmann committed Feb 23, 2021
1 parent dc4d1c5 commit a20992a
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 92 deletions.
63 changes: 46 additions & 17 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
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
Expand Down Expand Up @@ -100,23 +105,6 @@ func getMessageParts(msg string) (header string, bodyBlocks []string){
return
}

func parseMessageBlock(msg string, prefixes []string) shared.MessageBlock {
for _, prefix := range prefixes {
if !strings.HasPrefix(msg, prefix + ":") {
continue
}
content := strings.Replace(msg, prefix+":", "", 1)
return shared.MessageBlock{
Label: prefix,
Content: strings.TrimSpace(content),
}
}
return shared.MessageBlock{
Label: "",
Content: msg,
}
}

//
// getRegexMatchedMap will match a regex with named groups and map the matching
// results to corresponding group names
Expand All @@ -133,3 +121,44 @@ func getRegexMatchedMap(regEx, url string) (paramsMap map[string]string) {
}
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 "", ""
}
94 changes: 58 additions & 36 deletions internal/analyzer/conventional.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package analyzer

import (
"bufio"
"github.com/Nightapes/go-semantic-release/pkg/config"
"strings"

Expand All @@ -19,8 +20,7 @@ type conventional struct {

// CONVENTIONAL identifier
const CONVENTIONAL = "conventional"
const breakingChangeKeywords = "BREAKING CHANGE"
const breakingChangePrefix = breakingChangeKeywords + ":"
var conventionalFooterTokenSep = defaultTokenSeparators

func newConventional(config config.AnalyzerConfig) *conventional {
return &conventional{
Expand Down Expand Up @@ -91,34 +91,23 @@ func (a *conventional) getRules() []Rule {
}

func (a *conventional) analyze(commit shared.Commit, rule Rule) *shared.AnalyzedCommit {
prefixes := append(a.config.BlockPrefixes, breakingChangeKeywords)
tokenSep := append(a.config.TokenSeparators, conventionalFooterTokenSep[:]...)

firstSplit := strings.SplitN(commit.Message, "\n", 2)
header := firstSplit[0]
body := ""
if len(firstSplit) > 1 {
body = firstSplit[1]
}

header, txtBlocks := getMessageParts(commit.Message)
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 := make(map[string][]shared.MessageBlock)
footer := ""
if len(txtBlocks) > 0 {
bodyCount := len(txtBlocks)-1
if len(txtBlocks) == 1 {
bodyCount = 1
}
bodyTxtBlocks := txtBlocks[0:bodyCount]
if len(txtBlocks) > 1{
footer = txtBlocks[len(txtBlocks)-1]
}
msgBlockMap["body"] = getMessageBlocks(bodyTxtBlocks, prefixes)

if len(footer) > 0{
footerLines := strings.Split(footer, "\n")
msgBlockMap["footer"] = getMessageBlocks(footerLines, prefixes)
}
}
msgBlockMap := getConventionalMessageBlockMap(body, tokenSep)

analyzed := &shared.AnalyzedCommit{
Commit: commit,
Expand All @@ -129,23 +118,18 @@ func (a *conventional) analyze(commit shared.Commit, rule Rule) *shared.Analyzed
MessageBlocks: msgBlockMap,
}

isBreaking := matches["breaking"] == "!" || strings.Contains(commit.Message, breakingChangePrefix)
isBreaking := matches["breaking"] == "!" || strings.Contains(commit.Message, defaultBreakingChangePrefix)
analyzed.IsBreaking = isBreaking

oldMsgSplit := strings.SplitN(commit.Message, "\n", 2)
originalBodyBlock := ""
if len(oldMsgSplit) > 1 {
originalBodyBlock = oldMsgSplit[1]
}
oldFormatMessage := strings.TrimSpace(matches["subject"] + "\n" + originalBodyBlock)
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
}

a.log.Infof(" %s, BREAKING CHANGE found", commit.Message)
breakingChange := strings.SplitN(oldFormatMessage, breakingChangePrefix, 2)
breakingChange := strings.SplitN(oldFormatMessage, defaultBreakingChangePrefix, 2)

if len(breakingChange) > 1 {
analyzed.ParsedMessage = strings.TrimSpace(breakingChange[0])
Expand All @@ -157,12 +141,50 @@ func (a *conventional) analyze(commit shared.Commit, rule Rule) *shared.Analyzed
return analyzed
}

func getMessageBlocks(txtArray, prefixes []string) []shared.MessageBlock {
blocks := make([]shared.MessageBlock, len(txtArray))
for i, line := range txtArray{
blocks[i] = parseMessageBlock(line, prefixes)
func getConventionalMessageBlockMap(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))
}
return blocks
}

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
}
Loading

0 comments on commit a20992a

Please sign in to comment.