diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 17fc4a5..3cffc49 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -3,6 +3,8 @@ package analyzer import ( "fmt" + "regexp" + "strings" "github.com/Nightapes/go-semantic-release/internal/shared" "github.com/Nightapes/go-semantic-release/pkg/config" @@ -11,8 +13,9 @@ import ( // Analyzer struct type Analyzer struct { - analyzeCommits analyzeCommits - Config config.ChangelogConfig + analyzeCommits analyzeCommits + ChangelogConfig config.ChangelogConfig + AnalyzerConfig config.AnalyzerConfig } // Rule for commits @@ -24,14 +27,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 +43,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 +66,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 +84,52 @@ 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 } + +func getMessageParts(msg string) (header string, bodyBlocks []string){ + firstSplit := strings.SplitN(msg, "\n", 2) + header = firstSplit[0] + bodyBlocks = make([]string, 0) + + if len(firstSplit) < 2 { + return + } + // Trim and then split by a blank line + remaining := strings.Trim(firstSplit[1], "\n") + bodyBlocks = strings.Split(remaining, "\n\n") + + 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 +// +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 +} diff --git a/internal/analyzer/analyzer_test.go b/internal/analyzer/analyzer_test.go index 0002a73..d78cda4 100644 --- a/internal/analyzer/analyzer_test.go +++ b/internal/analyzer/analyzer_test.go @@ -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) } diff --git a/internal/analyzer/angular.go b/internal/analyzer/angular.go index b7c3784..cf9e348 100644 --- a/internal/analyzer/angular.go +++ b/internal/analyzer/angular.go @@ -86,12 +86,12 @@ func (a *angular) getRules() []Rule { return a.rules } -func (a *angular) analyze(commit shared.Commit, rule Rule) (*shared.AnalyzedCommit, bool) { +func (a *angular) analyze(commit shared.Commit, rule Rule) *shared.AnalyzedCommit { 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 + return nil } analyzed := &shared.AnalyzedCommit{ @@ -105,18 +105,21 @@ func (a *angular) analyze(commit shared.Commit, rule Rule) (*shared.AnalyzedComm if !strings.Contains(message, "BREAKING CHANGE:") { analyzed.ParsedMessage = strings.Trim(message, " ") 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) + analyzed.IsBreaking = true + if len(breakingChange) > 1 { analyzed.ParsedMessage = strings.TrimSpace(breakingChange[0]) analyzed.ParsedBreakingChangeMessage = strings.TrimSpace(breakingChange[1]) - return analyzed, true + + return analyzed } analyzed.ParsedBreakingChangeMessage = breakingChange[0] - return analyzed, true + return analyzed } diff --git a/internal/analyzer/angular_test.go b/internal/analyzer/angular_test.go index 250d5aa..527c2f6 100644 --- a/internal/analyzer/angular_test.go +++ b/internal/analyzer/angular_test.go @@ -75,6 +75,7 @@ func TestAngular(t *testing.T) { TagString: "Features", Print: true, ParsedBreakingChangeMessage: "change api to v2", + IsBreaking: true, }, }, "patch": {}, @@ -135,6 +136,7 @@ func TestAngular(t *testing.T) { TagString: "Features", Print: true, ParsedBreakingChangeMessage: "change api to v2", + IsBreaking: true, }, }, "patch": {}, @@ -212,7 +214,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 { diff --git a/internal/analyzer/conventional.go b/internal/analyzer/conventional.go index 4486a48..1fa448e 100644 --- a/internal/analyzer/conventional.go +++ b/internal/analyzer/conventional.go @@ -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,18 @@ type conventional struct { rules []Rule regex string log *log.Entry + config config.AnalyzerConfig } // CONVENTIONAL identifier const CONVENTIONAL = "conventional" +const breakingChangeKeywords = "BREAKING CHANGE" +const breakingChangePrefix = breakingChangeKeywords + ":" -func newConventional() *conventional { +func newConventional(config config.AnalyzerConfig) *conventional { return &conventional{ - regex: `^(TAG)(?:\((.*)\))?(\!)?: (?s)(.*)`, + config: config, + regex: `^(?P\w*)(?:\((?P.*)\))?(?P\!)?: (?P.*)`, log: log.WithField("analyzer", CONVENTIONAL), rules: []Rule{ { @@ -86,37 +90,79 @@ 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 { +func (a *conventional) analyze(commit shared.Commit, rule Rule) *shared.AnalyzedCommit { + prefixes := append(a.config.BlockPrefixes, breakingChangeKeywords) + + 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, false + 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) + } } 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, breakingChangePrefix) + 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) + 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, breakingChangePrefix, 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 } + +func getMessageBlocks(txtArray, prefixes []string) []shared.MessageBlock { + blocks := make([]shared.MessageBlock, len(txtArray)) + for i, line := range txtArray{ + blocks[i] = parseMessageBlock(line, prefixes) + } + return blocks +} + + diff --git a/internal/analyzer/conventional_test.go b/internal/analyzer/conventional_test.go index ca780f0..8d3ffcd 100644 --- a/internal/analyzer/conventional_test.go +++ b/internal/analyzer/conventional_test.go @@ -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{ + "body" : { 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{ + "body" : {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": {}, }, @@ -291,9 +328,53 @@ func TestConventional(t *testing.T) { }, }, }, + { + testCase: "fix issue with footers", + wantAnalyzedCommits: 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{ + "body": { shared.MessageBlock{ + Label: "Note", + Content: "now the logs will not print lines twice.", + }, + }, + "footer": { shared.MessageBlock{ + Label: "Issue", + Content: "#123", + }, + shared.MessageBlock{ + Label: "Severity", + Content: "medium", + }, + }, + }, + }}, + "minor": {}, + "major": {}, + "none": {}, + }, + 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", + }, + }, + }, } - conventional, err := analyzer.New("conventional", config.ChangelogConfig{}) + conventional, err := analyzer.New("conventional", config.AnalyzerConfig{BlockPrefixes: []string{"Note", "Issue", "Severity"}}, config.ChangelogConfig{}) assert.NoError(t, err) for _, test := range testConfigs { diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 4267836..225b5a7 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -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{}, }, }, }, diff --git a/internal/changelog/changelog.go b/internal/changelog/changelog.go index a2698f0..7df00bc 100644 --- a/internal/changelog/changelog.go +++ b/internal/changelog/changelog.go @@ -110,7 +110,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 } @@ -142,6 +142,7 @@ func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConf HasDockerLatest: c.config.Changelog.Docker.Latest, DockerRepository: c.config.Changelog.Docker.Repository, } + template := defaultCommitListSubTemplate + defaultChangelog if c.config.Changelog.TemplatePath != "" { content, err := ioutil.ReadFile(c.config.Changelog.TemplatePath) diff --git a/internal/changelog/changelog_test.go b/internal/changelog/changelog_test.go index 8ff146c..a5ad35c 100644 --- a/internal/changelog/changelog_test.go +++ b/internal/changelog/changelog_test.go @@ -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{}, }, }, }, diff --git a/internal/shared/shared.go b/internal/shared/shared.go index 5aee964..4be867b 100644 --- a/internal/shared/shared.go +++ b/internal/shared/shared.go @@ -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 diff --git a/pkg/config/config.go b/pkg/config/config.go index 091a7ff..ba4266f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -13,6 +13,11 @@ const ( DefaultTagPrefix = "v" ) +// AnalyzerConfig struct +type AnalyzerConfig struct { + BlockPrefixes []string `yaml:"blockPrefixes"` +} + // 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"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 65bbc00..27cf14c 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -100,6 +100,7 @@ github: Compress: false}}, ReleaseTitle: "go-semantic-release release", IsPreRelease: false, + Analyzer: config.AnalyzerConfig{BlockPrefixes: []string{}}, }, result) } diff --git a/pkg/semanticrelease/semantic-release.go b/pkg/semanticrelease/semantic-release.go index eb1d67d..5eb174b 100644 --- a/pkg/semanticrelease/semantic-release.go +++ b/pkg/semanticrelease/semantic-release.go @@ -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 }