Merge remote-tracking branch 'origin/travis' into add_git_releases

This commit is contained in:
Felix Wiedmann
2019-05-28 18:47:44 +02:00
9 changed files with 313 additions and 107 deletions

View File

@@ -1,11 +1,11 @@
commitFormat: angular commitFormat: angular
branch: branch:
master: release master: release
travis: rc rc: rc
beta: beta beta: beta
alpha: alpha alpha: alpha
changelog: changelog:
print: all/compact printAll: false
template: '' template: ''
templatePath: '' templatePath: ''
release: 'github' release: 'github'

2
README2.md Normal file
View File

@@ -0,0 +1,2 @@
go build ./cmd/main.go && ./main.exe version next --path /f/Repro/ambassador/
go build ./cmd/main.go && ./main.exe --loglevel debug version set v1.1.1 --path /f/Repro/ambassador/

View File

@@ -2,6 +2,7 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/Nightapes/go-semantic-release/pkg/config" "github.com/Nightapes/go-semantic-release/pkg/config"
@@ -24,6 +25,11 @@ var (
setRepository = setCommand.Flag("repository", "Path to repository").String() setRepository = setCommand.Flag("repository", "Path to repository").String()
setConfigPath = setCommand.Flag("config", "Path to config file").Default(".release.yml").String() setConfigPath = setCommand.Flag("config", "Path to config file").Default(".release.yml").String()
setVersion = setCommand.Arg("version", "semver version").Required().String() setVersion = setCommand.Arg("version", "semver version").Required().String()
getChangelog = app.Command("changelog", "Print changelog.")
getChangelogRepository = getChangelog.Flag("repository", "Path to repository").String()
getChangelogConfigPath = getChangelog.Flag("config", "Path to config file").Default(".release.yml").String()
getChangelogFile = getChangelog.Flag("file", "save changelog to file").Default("CHANGELOG.md").String()
) )
func main() { func main() {
@@ -32,10 +38,11 @@ func main() {
case nextCommand.FullCommand(): case nextCommand.FullCommand():
setLoglevel(*loglevel) setLoglevel(*loglevel)
s := semanticrelease.New(readConfig(nextConfigPath)) s := semanticrelease.New(readConfig(nextConfigPath))
err := s.GetNextVersion(*nextRepository, *nextForce) version, err := s.GetNextVersion(*nextRepository, *nextForce)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println(version)
case setCommand.FullCommand(): case setCommand.FullCommand():
setLoglevel(*loglevel) setLoglevel(*loglevel)
@@ -45,6 +52,13 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
case getChangelog.FullCommand():
setLoglevel(*loglevel)
s := semanticrelease.New(readConfig(getChangelogConfigPath))
err := s.GetChangelog(*getChangelogRepository, *getChangelogFile)
if err != nil {
log.Fatal(err)
}
} }
} }

View File

@@ -3,24 +3,27 @@ package analyzer
import ( import (
"github.com/Nightapes/go-semantic-release/internal/gitutil" "github.com/Nightapes/go-semantic-release/internal/gitutil"
"github.com/Nightapes/go-semantic-release/pkg/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
//Analyzer struct //Analyzer struct
type Analyzer struct { type Analyzer struct {
CommitFormat string CommitFormat string
Config config.ChangelogConfig
} }
//Rules for commits //Rule for commits
type Rules struct { type Rule struct {
Tag string Tag string
Release string TagString string
Enabled bool Release string
Changelog bool
} }
type analyzeCommit interface { type analyzeCommit interface {
analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bool) analyze(commit gitutil.Commit, tag Rule) (AnalyzedCommit, bool, error)
getRules() []Rules getRules() []Rule
} }
//AnalyzedCommit struct //AnalyzedCommit struct
@@ -29,12 +32,16 @@ type AnalyzedCommit struct {
ParsedMessage string ParsedMessage string
Scope string Scope string
ParsedBreakingChangeMessage string ParsedBreakingChangeMessage string
Tag string
TagString string
Print bool
} }
//New Analyzer struct for given commit format //New Analyzer struct for given commit format
func New(format string) *Analyzer { func New(format string, config config.ChangelogConfig) *Analyzer {
return &Analyzer{ return &Analyzer{
CommitFormat: format, CommitFormat: format,
Config: config,
} }
} }
@@ -45,7 +52,7 @@ func (a *Analyzer) Analyze(commits []gitutil.Commit) map[string][]AnalyzedCommit
var commitAnalayzer analyzeCommit var commitAnalayzer analyzeCommit
switch a.CommitFormat { switch a.CommitFormat {
case "angular": case "angular":
log.Infof("analyze angular format") log.Debugf("Commit format set to angular")
commitAnalayzer = newAngular() commitAnalayzer = newAngular()
} }
@@ -53,18 +60,29 @@ func (a *Analyzer) Analyze(commits []gitutil.Commit) map[string][]AnalyzedCommit
analyzedCommits["major"] = make([]AnalyzedCommit, 0) analyzedCommits["major"] = make([]AnalyzedCommit, 0)
analyzedCommits["minor"] = make([]AnalyzedCommit, 0) analyzedCommits["minor"] = make([]AnalyzedCommit, 0)
analyzedCommits["patch"] = make([]AnalyzedCommit, 0) analyzedCommits["patch"] = make([]AnalyzedCommit, 0)
analyzedCommits["none"] = make([]AnalyzedCommit, 0)
for _, commit := range commits { for _, commit := range commits {
for _, rule := range commitAnalayzer.getRules() { for _, rule := range commitAnalayzer.getRules() {
analyzedCommit, hasBreakingChange := commitAnalayzer.analyze(commit, rule.Tag) analyzedCommit, hasBreakingChange, err := commitAnalayzer.analyze(commit, rule)
if hasBreakingChange { if err == nil {
analyzedCommits["major"] = append(analyzedCommits["major"], analyzedCommit) if a.Config.PrintAll {
} else { analyzedCommit.Print = true
analyzedCommits[rule.Release] = append(analyzedCommits[rule.Release], analyzedCommit) } else {
analyzedCommit.Print = rule.Changelog
}
if hasBreakingChange {
analyzedCommits["major"] = append(analyzedCommits["major"], analyzedCommit)
} else {
analyzedCommits[rule.Release] = append(analyzedCommits[rule.Release], analyzedCommit)
}
break
} }
} }
} }
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 return analyzedCommits
} }

View File

@@ -2,53 +2,93 @@
package analyzer package analyzer
import ( import (
"fmt"
"regexp" "regexp"
"strings" "strings"
log "github.com/sirupsen/logrus"
"github.com/Nightapes/go-semantic-release/internal/gitutil" "github.com/Nightapes/go-semantic-release/internal/gitutil"
) )
type angular struct { type angular struct {
rules []Rules rules []Rule
regex string regex string
} }
func newAngular() *angular { func newAngular() *angular {
return &angular{ return &angular{
regex: `(TAG)(?:\((.*)\))?: (.*)`, regex: `(TAG)(?:\((.*)\))?: (.*)`,
rules: []Rules{ rules: []Rule{
{ {
Tag: "feat", Tag: "feat",
Release: "minor", TagString: "Features",
Enabled: true, Release: "minor",
Changelog: true,
}, },
{ {
Tag: "fix", Tag: "fix",
Release: "patch", TagString: "Bug fixes",
Enabled: true, Release: "patch",
Changelog: true,
}, { }, {
Tag: "perf", Tag: "perf",
Release: "patch", TagString: "Performance improvments",
Enabled: true, Release: "patch",
Changelog: true,
}, {
Tag: "docs",
TagString: "Documentation changes",
Release: "none",
Changelog: false,
},
{
Tag: "style",
TagString: "Style",
Release: "none",
Changelog: false,
}, {
Tag: "refactor",
TagString: "Code refactor",
Release: "none",
Changelog: false,
}, {
Tag: "test",
TagString: "Testing",
Release: "none",
Changelog: false,
}, {
Tag: "chore",
TagString: "Changes to the build process or auxiliary tools and libraries such as documentation generation",
Release: "none",
Changelog: false,
}, {
Tag: "build",
TagString: "Changes to ci config",
Release: "none",
Changelog: false,
}, },
}, },
} }
} }
func (a *angular) getRules() []Rules { func (a *angular) getRules() []Rule {
return a.rules return a.rules
} }
func (a *angular) analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bool) { func (a *angular) analyze(commit gitutil.Commit, rule Rule) (AnalyzedCommit, bool, error) {
analyzed := AnalyzedCommit{ analyzed := AnalyzedCommit{
Commit: commit, Commit: commit,
Tag: rule.Tag,
TagString: rule.TagString,
} }
re := regexp.MustCompile(strings.Replace(a.regex, "TAG", tag, -1)) re := regexp.MustCompile(strings.Replace(a.regex, "TAG", rule.Tag, -1))
matches := re.FindAllStringSubmatch(commit.Message+" "+commit.Message, -1) matches := re.FindAllStringSubmatch(commit.Message, -1)
if len(matches) >= 1 { if len(matches) >= 1 {
if len(matches[0]) >= 3 { if len(matches[0]) >= 3 {
analyzed.Scope = matches[0][2] analyzed.Scope = matches[0][2]
message := strings.Join(matches[0][3:], "") message := strings.Join(matches[0][3:], "")
@@ -56,14 +96,17 @@ func (a *angular) analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bo
if len(splitted) == 1 { if len(splitted) == 1 {
analyzed.ParsedMessage = splitted[0] analyzed.ParsedMessage = splitted[0]
return analyzed, false log.Tracef("%s: found %s", commit.Message, rule.Tag)
return analyzed, false, nil
} }
analyzed.ParsedMessage = splitted[0] analyzed.ParsedMessage = splitted[0]
analyzed.ParsedBreakingChangeMessage = splitted[1] analyzed.ParsedBreakingChangeMessage = splitted[1]
return analyzed, true log.Tracef(" %s, BREAKING CHANGE found", commit.Message)
return analyzed, true, nil
} }
} }
return analyzed, false log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag)
return analyzed, false, fmt.Errorf("Not found")
} }

View File

@@ -0,0 +1,82 @@
package changelog
import (
"bytes"
"text/template"
"time"
"github.com/Nightapes/go-semantic-release/internal/analyzer"
"github.com/Nightapes/go-semantic-release/pkg/config"
)
const defaultChangelogTitle string = `v{{.Version}} ({{.Now.Format "2006-01-02"}})`
const defaultChangelog string = `{{ $version := .Version -}}
{{ $backtick := .Backtick -}}
# v{{.Version}} ({{.Now.Format "2006-01-02"}})
{{ range $key, $commits := .Commits }}
### {{ $key }}
{{range $index,$commit := $commits}}* **{{$backtick}}{{$commit.Scope}}:{{$backtick}}** {{$commit.ParsedMessage}}
{{ end -}}
{{ end -}}
`
type changelogContent struct {
Commits map[string][]analyzer.AnalyzedCommit
Version string
Now time.Time
Backtick string
}
//CommitFormat struct
type Changelog struct {
config *config.ReleaseConfig
}
//New Changelog struct for generating changelog from commits
func New(config *config.ReleaseConfig) *Changelog {
return &Changelog{
config: config,
}
}
// GenerateChanglog from given commits
func (c *Changelog) GenerateChanglog(version string, analyzedCommits map[string][]analyzer.AnalyzedCommit) (string, string, error) {
commitsPerScope := map[string][]analyzer.AnalyzedCommit{}
for _, commits := range analyzedCommits {
for _, commit := range commits {
if commit.Print {
if _, ok := commitsPerScope[commit.TagString]; !ok {
commitsPerScope[commit.TagString] = make([]analyzer.AnalyzedCommit, 0)
}
commitsPerScope[commit.TagString] = append(commitsPerScope[commit.TagString], commit)
}
}
}
changelogContent := changelogContent{
Version: version,
Commits: commitsPerScope,
Now: time.Now(),
Backtick: "`",
}
title, err := generateTemplate(defaultChangelogTitle, changelogContent)
content, err := generateTemplate(defaultChangelog, changelogContent)
return title, content, err
}
func generateTemplate(text string, values changelogContent) (string, error) {
var tpl bytes.Buffer
tmpl, err := template.New("template").Parse(text)
if err != nil {
return "", nil
}
err = tmpl.Execute(&tpl, values)
if err != nil {
return "", nil
}
return tpl.String(), nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object" "gopkg.in/src-d/go-git.v4/plumbing/object"
) )
@@ -62,23 +63,27 @@ func (g *GitUtil) GetBranch() (string, error) {
// GetLastVersion from git tags // GetLastVersion from git tags
func (g *GitUtil) GetLastVersion() (*semver.Version, string, error) { func (g *GitUtil) GetLastVersion() (*semver.Version, string, error) {
log.Debugf("GetLastVersion") var tags []*semver.Version
gitTags, err := g.Repository.Tags()
tagObjects, err := g.Repository.TagObjects()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
var tags []*semver.Version err = gitTags.ForEach(func(p *plumbing.Reference) error {
v, err := semver.NewVersion(p.Name().Short())
err = tagObjects.ForEach(func(t *object.Tag) error { log.Tracef("%+v", p.Name().Short())
v, err := semver.NewVersion(t.Name) if err == nil {
_, err := g.Repository.TagObject(p.Hash())
if err != nil { if err == nil {
log.Debugf("Tag %s is not a valid version, skip", t.Name) log.Debugf("Add tag %s", p.Name().Short())
tags = append(tags, v)
} else {
log.Debugf("Found tag %s, but is not annotated, skip", err.Error())
}
} else { } else {
log.Debugf("Add tag %s", t.Name) log.Debugf("Tag %s is not a valid version, skip", p.Name().Short())
tags = append(tags, v)
} }
return nil return nil
}) })
@@ -113,7 +118,6 @@ func (g *GitUtil) GetLastVersion() (*semver.Version, string, error) {
// GetCommits from git hash to HEAD // GetCommits from git hash to HEAD
func (g *GitUtil) GetCommits(lastTagHash string) ([]Commit, error) { func (g *GitUtil) GetCommits(lastTagHash string) ([]Commit, error) {
log.Printf("Read head")
ref, err := g.Repository.Head() ref, err := g.Repository.Head()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -129,7 +133,7 @@ func (g *GitUtil) GetCommits(lastTagHash string) ([]Commit, error) {
err = cIter.ForEach(func(c *object.Commit) error { err = cIter.ForEach(func(c *object.Commit) error {
if c.Hash.String() == lastTagHash { if c.Hash.String() == lastTagHash {
log.Infof("%s == %s", c.Hash.String(), lastTagHash) log.Debugf("Found commit with hash %s, will stop here", c.Hash.String())
foundEnd = true foundEnd = true
} }

View File

@@ -10,7 +10,7 @@ import (
// ChangelogConfig struct // ChangelogConfig struct
type ChangelogConfig struct { type ChangelogConfig struct {
Print string `yaml:"print,omitempty"` PrintAll bool `yaml:"printAll,omitempty"`
Template string `yaml:"template,omitempty"` Template string `yaml:"template,omitempty"`
TemplatePath string `yaml:"templatePath,omitempty"` TemplatePath string `yaml:"templatePath,omitempty"`
} }

View File

@@ -2,13 +2,14 @@
package semanticrelease package semanticrelease
import ( import (
"fmt" "io/ioutil"
"strconv" "strconv"
"strings" "strings"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"github.com/Nightapes/go-semantic-release/internal/analyzer" "github.com/Nightapes/go-semantic-release/internal/analyzer"
"github.com/Nightapes/go-semantic-release/internal/cache" "github.com/Nightapes/go-semantic-release/internal/cache"
"github.com/Nightapes/go-semantic-release/internal/changelog"
"github.com/Nightapes/go-semantic-release/internal/gitutil" "github.com/Nightapes/go-semantic-release/internal/gitutil"
"github.com/Nightapes/go-semantic-release/pkg/config" "github.com/Nightapes/go-semantic-release/pkg/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -27,15 +28,15 @@ func New(c *config.ReleaseConfig) *SemanticRelease {
} }
// GetNextVersion from .version or calculate new from commits // GetNextVersion from .version or calculate new from commits
func (s *SemanticRelease) GetNextVersion(repro string, force bool) error { func (s *SemanticRelease) GetNextVersion(repro string, force bool) (string, error) {
util, err := gitutil.New(repro) util, err := gitutil.New(repro)
if err != nil { if err != nil {
return err return "", err
} }
hash, err := util.GetHash() hash, err := util.GetHash()
if err != nil { if err != nil {
return err return "", err
} }
log.Debugf("Ignore .version file if exits, %t", force) log.Debugf("Ignore .version file if exits, %t", force)
@@ -43,89 +44,67 @@ func (s *SemanticRelease) GetNextVersion(repro string, force bool) error {
content, err := cache.Read() content, err := cache.Read()
if err == nil && content.Commit == hash { if err == nil && content.Commit == hash {
fmt.Printf(content.NextVersion) log.Infof("Found cache, will return cached version %s", content.NextVersion)
return nil return content.NextVersion, err
} }
log.Debugf("Mismatch git and version file %s - %s", content.Commit, hash) log.Debugf("Mismatch git and version file %s - %s", content.Commit, hash)
} }
lastVersion, lastVersionHash, err := util.GetLastVersion() lastVersion, lastVersionHash, err := util.GetLastVersion()
if err != nil { if err != nil {
return err return "", err
} }
var newVersion semver.Version
if lastVersion == nil { if lastVersion == nil {
defaultVersion, _ := semver.NewVersion("1.0.0") defaultVersion, _ := semver.NewVersion("1.0.0")
err := s.SetVersion(defaultVersion.String(), repro) newVersion = *defaultVersion
if err != nil { } else {
return err newVersion = *lastVersion
}
fmt.Printf("%s", defaultVersion.String())
return nil
} }
commits, err := util.GetCommits(lastVersionHash) commits, err := util.GetCommits(lastVersionHash)
if err != nil { if err != nil {
return err return "", err
} }
log.Debugf("Found %d commits till last release", len(commits)) log.Debugf("Found %d commits till last release", len(commits))
a := analyzer.New("angular") a := analyzer.New(s.config.CommitFormat, s.config.Changelog)
result := a.Analyze(commits) result := a.Analyze(commits)
var newVersion semver.Version
currentBranch, err := util.GetBranch() currentBranch, err := util.GetBranch()
if err != nil { if err != nil {
return err return "", err
}
newVersion = *lastVersion
if lastVersion.Prerelease() == "" {
if len(result["major"]) > 0 {
newVersion = lastVersion.IncMajor()
} else if len(result["minor"]) > 0 {
newVersion = lastVersion.IncMinor()
} else if len(result["patch"]) > 0 {
newVersion = lastVersion.IncPatch()
}
} }
log.Debugf("Test %+v", s.config)
for branch, releaseType := range s.config.Branch { for branch, releaseType := range s.config.Branch {
if currentBranch == branch || strings.HasPrefix(currentBranch, branch) { if currentBranch == branch || strings.HasPrefix(currentBranch, branch) {
log.Debugf("Found branch config for branch %s with release type %s", currentBranch, releaseType) log.Debugf("Found branch config for branch %s with release type %s", currentBranch, releaseType)
switch releaseType { switch releaseType {
case "rc": case "rc", "beta", "alpha":
if newVersion.Prerelease() == "" || !strings.HasPrefix(newVersion.Prerelease(), "rc") { newVersion = incPrerelease(releaseType, newVersion)
newVersion, _ = newVersion.SetPrerelease("rc.0") case "release":
} else { if len(result["major"]) > 0 {
parts := strings.Split(newVersion.Prerelease(), ".") newVersion = newVersion.IncMajor()
if len(parts) == 2 { } else if len(result["minor"]) > 0 {
i, err := strconv.Atoi(parts[1]) newVersion = newVersion.IncMinor()
if err != nil { } else if len(result["patch"]) > 0 {
newVersion, _ = newVersion.SetPrerelease("rc.0") newVersion = newVersion.IncPatch()
log.Warnf("Could not parse release tag %s, use version %s", newVersion.Prerelease(), newVersion.String())
} else {
newVersion, _ = newVersion.SetPrerelease("rc." + strconv.Itoa((i + 1)))
}
} else {
newVersion, _ = newVersion.SetPrerelease("rc.0")
log.Warnf("Could not parse release tag %s, use version %s", newVersion.Prerelease(), newVersion.String())
}
} }
} }
} }
} }
err = s.SetVersion(newVersion.String(), repro) log.Infof("New version %s -> %s", lastVersion.String(), newVersion.String())
err = saveToCache(util, lastVersion, &newVersion)
if err != nil { if err != nil {
return err return "", err
} }
fmt.Printf("%s", newVersion.String()) c := changelog.New(s.config)
c.GenerateChanglog(newVersion.String(), result)
return err return newVersion.String(), err
} }
//SetVersion for git repository //SetVersion for git repository
@@ -141,6 +120,16 @@ func (s *SemanticRelease) SetVersion(version string, repro string) error {
return err return err
} }
lastVersion, _, err := util.GetLastVersion()
if err != nil {
return err
}
return saveToCache(util, lastVersion, newVersion)
}
func saveToCache(util *gitutil.GitUtil, lastVersion *semver.Version, nextVersion *semver.Version) error {
hash, err := util.GetHash() hash, err := util.GetHash()
if err != nil { if err != nil {
return err return err
@@ -153,18 +142,72 @@ func (s *SemanticRelease) SetVersion(version string, repro string) error {
newVersionContent := cache.VersionFileContent{ newVersionContent := cache.VersionFileContent{
Commit: hash, Commit: hash,
NextVersion: newVersion.String(), NextVersion: nextVersion.String(),
Branch: branch, Branch: branch,
} }
lastVersion, _, err := util.GetLastVersion()
if err != nil {
return err
}
if lastVersion != nil { if lastVersion != nil {
newVersionContent.Version = lastVersion.String() newVersionContent.Version = lastVersion.String()
} }
log.Debugf("Save %s with hash %s to cache", nextVersion.String(), hash)
return cache.Write(newVersionContent) return cache.Write(newVersionContent)
} }
func incPrerelease(preReleaseType string, version semver.Version) semver.Version {
defaultPrerelease := preReleaseType + ".0"
if version.Prerelease() == "" || !strings.HasPrefix(version.Prerelease(), preReleaseType) {
version, _ = version.SetPrerelease(defaultPrerelease)
} else {
parts := strings.Split(version.Prerelease(), ".")
if len(parts) == 2 {
i, err := strconv.Atoi(parts[1])
if err != nil {
version, _ = version.SetPrerelease(defaultPrerelease)
log.Warnf("Could not parse release tag %s, use version %s", version.Prerelease(), version.String())
} else {
version, _ = version.SetPrerelease(preReleaseType + "." + strconv.Itoa((i + 1)))
}
} else {
version, _ = version.SetPrerelease(defaultPrerelease)
log.Warnf("Could not parse release tag %s, use version %s", version.Prerelease(), version.String())
}
}
return version
}
// GetChangelog from last version till now
func (s *SemanticRelease) GetChangelog(repro, file string) error {
nextVersion, err := s.GetNextVersion(repro, false)
if err != nil {
log.Debugf("Could not get next version")
return err
}
util, err := gitutil.New(repro)
if err != nil {
return err
}
_, lastVersionHash, err := util.GetLastVersion()
if err != nil {
return err
}
commits, err := util.GetCommits(lastVersionHash)
if err != nil {
return err
}
log.Debugf("Found %d commits till last release", len(commits))
a := analyzer.New(s.config.CommitFormat, s.config.Changelog)
result := a.Analyze(commits)
c := changelog.New(s.config)
_, content, err := c.GenerateChanglog(nextVersion, result)
return ioutil.WriteFile(file, []byte(content), 0644)
}