From fc7e6366c2884ccdfd84eb0690f75e7fa0976fca Mon Sep 17 00:00:00 2001 From: Nightapes Date: Sat, 25 May 2019 18:10:24 +0200 Subject: [PATCH] feat(changelog): add first draft for changelog generation --- .release.yml | 8 +-- cmd/go-semantic-release/main.go | 16 ++++- internal/analyzer/analyzer.go | 23 +++++-- internal/analyzer/angular.go | 29 ++++++--- internal/changelog/changelog.go | 82 +++++++++++++++++++++++++ pkg/config/config.go | 2 +- pkg/semanticrelease/semantic-release.go | 62 +++++++++++++++---- 7 files changed, 188 insertions(+), 34 deletions(-) create mode 100644 internal/changelog/changelog.go diff --git a/.release.yml b/.release.yml index 3a1f903..730a27a 100644 --- a/.release.yml +++ b/.release.yml @@ -1,11 +1,11 @@ commitFormat: angular branch: master: release - sd: rc - sds: beta - travis: alpha + rc: rc + beta: beta + alpha: alpha changelog: - print: all/compact + printAll: false template: '' templatePath: '' release: 'github' diff --git a/cmd/go-semantic-release/main.go b/cmd/go-semantic-release/main.go index 7bf2848..d7cfd3a 100644 --- a/cmd/go-semantic-release/main.go +++ b/cmd/go-semantic-release/main.go @@ -2,6 +2,7 @@ package main import ( + "fmt" "os" "github.com/Nightapes/go-semantic-release/pkg/config" @@ -24,6 +25,11 @@ var ( setRepository = setCommand.Flag("repository", "Path to repository").String() setConfigPath = setCommand.Flag("config", "Path to config file").Default(".release.yml").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() { @@ -32,10 +38,11 @@ func main() { case nextCommand.FullCommand(): setLoglevel(*loglevel) s := semanticrelease.New(readConfig(nextConfigPath)) - err := s.GetNextVersion(*nextRepository, *nextForce) + version, err := s.GetNextVersion(*nextRepository, *nextForce) if err != nil { log.Fatal(err) } + fmt.Println(version) case setCommand.FullCommand(): setLoglevel(*loglevel) @@ -45,6 +52,13 @@ func main() { if err != nil { log.Fatal(err) } + case getChangelog.FullCommand(): + setLoglevel(*loglevel) + s := semanticrelease.New(readConfig(getChangelogConfigPath)) + err := s.GetChangelog(*getChangelogRepository, *getChangelogFile) + if err != nil { + log.Fatal(err) + } } } diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 541dd55..10f2cc3 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -3,24 +3,27 @@ package analyzer import ( "github.com/Nightapes/go-semantic-release/internal/gitutil" + "github.com/Nightapes/go-semantic-release/pkg/config" log "github.com/sirupsen/logrus" ) //Analyzer struct type Analyzer struct { CommitFormat string + Config config.ChangelogConfig } -//Rules for commits -type Rules struct { +//Rule for commits +type Rule struct { Tag string + TagString string Release string Changelog bool } type analyzeCommit interface { - analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bool, error) - getRules() []Rules + analyze(commit gitutil.Commit, tag Rule) (AnalyzedCommit, bool, error) + getRules() []Rule } //AnalyzedCommit struct @@ -30,12 +33,15 @@ type AnalyzedCommit struct { Scope string ParsedBreakingChangeMessage string Tag string + TagString string + Print bool } //New Analyzer struct for given commit format -func New(format string) *Analyzer { +func New(format string, config config.ChangelogConfig) *Analyzer { return &Analyzer{ CommitFormat: format, + Config: config, } } @@ -58,8 +64,13 @@ func (a *Analyzer) Analyze(commits []gitutil.Commit) map[string][]AnalyzedCommit for _, commit := range commits { for _, rule := range commitAnalayzer.getRules() { - analyzedCommit, hasBreakingChange, err := commitAnalayzer.analyze(commit, rule.Tag) + analyzedCommit, hasBreakingChange, err := commitAnalayzer.analyze(commit, rule) if err == nil { + if a.Config.PrintAll { + analyzedCommit.Print = true + } else { + analyzedCommit.Print = rule.Changelog + } if hasBreakingChange { analyzedCommits["major"] = append(analyzedCommits["major"], analyzedCommit) } else { diff --git a/internal/analyzer/angular.go b/internal/analyzer/angular.go index b73354a..41e09c0 100644 --- a/internal/analyzer/angular.go +++ b/internal/analyzer/angular.go @@ -12,50 +12,59 @@ import ( ) type angular struct { - rules []Rules + rules []Rule regex string } func newAngular() *angular { return &angular{ regex: `(TAG)(?:\((.*)\))?: (.*)`, - rules: []Rules{ + rules: []Rule{ { Tag: "feat", + TagString: "Features", Release: "minor", Changelog: true, }, { Tag: "fix", + TagString: "Bug fixes", Release: "patch", Changelog: true, }, { Tag: "perf", + TagString: "Performance improvments", 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, }, @@ -63,21 +72,23 @@ func newAngular() *angular { } } -func (a *angular) getRules() []Rules { +func (a *angular) getRules() []Rule { return a.rules } -func (a *angular) analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bool, error) { +func (a *angular) analyze(commit gitutil.Commit, rule Rule) (AnalyzedCommit, bool, error) { analyzed := AnalyzedCommit{ - Commit: commit, - Tag: tag, + 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, -1) if len(matches) >= 1 { if len(matches[0]) >= 3 { + analyzed.Scope = matches[0][2] message := strings.Join(matches[0][3:], "") @@ -85,7 +96,7 @@ func (a *angular) analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bo if len(splitted) == 1 { analyzed.ParsedMessage = splitted[0] - log.Tracef("%s: found %s", commit.Message, tag) + log.Tracef("%s: found %s", commit.Message, rule.Tag) return analyzed, false, nil } analyzed.ParsedMessage = splitted[0] @@ -95,7 +106,7 @@ func (a *angular) analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bo } } - log.Tracef("%s does not match %s, skip", commit.Message, tag) + log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag) return analyzed, false, fmt.Errorf("Not found") } diff --git a/internal/changelog/changelog.go b/internal/changelog/changelog.go new file mode 100644 index 0000000..75ffd73 --- /dev/null +++ b/internal/changelog/changelog.go @@ -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 +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 2ad014c..44d56d2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,7 +10,7 @@ import ( // ChangelogConfig struct type ChangelogConfig struct { - Print string `yaml:"print,omitempty"` + PrintAll bool `yaml:"printAll,omitempty"` Template string `yaml:"template,omitempty"` TemplatePath string `yaml:"templatePath,omitempty"` } diff --git a/pkg/semanticrelease/semantic-release.go b/pkg/semanticrelease/semantic-release.go index 94b1de2..f44b27c 100644 --- a/pkg/semanticrelease/semantic-release.go +++ b/pkg/semanticrelease/semantic-release.go @@ -2,13 +2,14 @@ package semanticrelease import ( - "fmt" + "io/ioutil" "strconv" "strings" "github.com/Masterminds/semver" "github.com/Nightapes/go-semantic-release/internal/analyzer" "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/pkg/config" log "github.com/sirupsen/logrus" @@ -27,15 +28,15 @@ func New(c *config.ReleaseConfig) *SemanticRelease { } // 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) if err != nil { - return err + return "", err } hash, err := util.GetHash() if err != nil { - return err + return "", err } log.Debugf("Ignore .version file if exits, %t", force) @@ -44,15 +45,14 @@ func (s *SemanticRelease) GetNextVersion(repro string, force bool) error { if err == nil && content.Commit == hash { log.Infof("Found cache, will return cached version %s", content.NextVersion) - fmt.Printf(content.NextVersion) - return nil + return content.NextVersion, err } log.Debugf("Mismatch git and version file %s - %s", content.Commit, hash) } lastVersion, lastVersionHash, err := util.GetLastVersion() if err != nil { - return err + return "", err } var newVersion semver.Version @@ -65,17 +65,17 @@ func (s *SemanticRelease) GetNextVersion(repro string, force bool) error { commits, err := util.GetCommits(lastVersionHash) if err != nil { - return err + return "", err } 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) currentBranch, err := util.GetBranch() if err != nil { - return err + return "", err } for branch, releaseType := range s.config.Branch { @@ -99,11 +99,12 @@ func (s *SemanticRelease) GetNextVersion(repro string, force bool) error { log.Infof("New version %s -> %s", lastVersion.String(), newVersion.String()) err = saveToCache(util, lastVersion, &newVersion) 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 @@ -175,3 +176,38 @@ func incPrerelease(preReleaseType string, version semver.Version) semver.Version 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) + +}