From 2cd32e3ed3c36272e99e8908e2f4058e1eaf98dc Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sat, 1 Feb 2020 21:00:53 -0300 Subject: [PATCH 1/8] refactor: add version to releasenote struct --- cmd/git-sv/handlers.go | 4 ++-- sv/helpers_test.go | 3 ++- sv/releasenotes.go | 15 ++++++++------- sv/releasenotes_test.go | 18 ++++++++++++------ 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index 7615184..f6da003 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -93,8 +93,8 @@ func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, return err } - releasenote := rnProcessor.Get(date, commits) - fmt.Println(rnProcessor.Format(releasenote, rnVersion)) + releasenote := rnProcessor.Create(rnVersion, date, commits) + fmt.Println(rnProcessor.Format(releasenote)) return nil } } diff --git a/sv/helpers_test.go b/sv/helpers_test.go index 6d54f11..d1c0273 100644 --- a/sv/helpers_test.go +++ b/sv/helpers_test.go @@ -19,8 +19,9 @@ func commitlog(t string, metadata map[string]string) GitCommitLog { } } -func releaseNote(date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote { +func releaseNote(version semver.Version, date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote { return ReleaseNote{ + Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges, diff --git a/sv/releasenotes.go b/sv/releasenotes.go index 991c3a4..5389f1f 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -33,8 +33,8 @@ const markdownTemplate = `## v{{.Version}} ({{.Date}}) // ReleaseNoteProcessor release note processor interface. type ReleaseNoteProcessor interface { - Get(date time.Time, commits []GitCommitLog) ReleaseNote - Format(releasenote ReleaseNote, version semver.Version) string + Create(version semver.Version, date time.Time, commits []GitCommitLog) ReleaseNote + Format(releasenote ReleaseNote) string } // ReleaseNoteProcessorImpl release note based on commit log. @@ -49,8 +49,8 @@ func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl { return &ReleaseNoteProcessorImpl{tags: tags, template: template} } -// Get generate a release note based on commits. -func (p ReleaseNoteProcessorImpl) Get(date time.Time, commits []GitCommitLog) ReleaseNote { +// Create create a release note based on commits. +func (p ReleaseNoteProcessorImpl) Create(version semver.Version, date time.Time, commits []GitCommitLog) ReleaseNote { sections := make(map[string]ReleaseNoteSection) var breakingChanges []string for _, commit := range commits { @@ -67,13 +67,13 @@ func (p ReleaseNoteProcessorImpl) Get(date time.Time, commits []GitCommitLog) Re } } - return ReleaseNote{Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges} + return ReleaseNote{Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges} } // Format format a release note. -func (p ReleaseNoteProcessorImpl) Format(releasenote ReleaseNote, version semver.Version) string { +func (p ReleaseNoteProcessorImpl) Format(releasenote ReleaseNote) string { templateVars := releaseNoteTemplate{ - Version: fmt.Sprintf("%d.%d.%d", version.Major(), version.Minor(), version.Patch()), + Version: fmt.Sprintf("%d.%d.%d", releasenote.Version.Major(), releasenote.Version.Minor(), releasenote.Version.Patch()), Date: releasenote.Date.Format("2006-01-02"), Sections: releasenote.Sections, BreakingChanges: releasenote.BreakingChanges, @@ -86,6 +86,7 @@ func (p ReleaseNoteProcessorImpl) Format(releasenote ReleaseNote, version semver // ReleaseNote release note. type ReleaseNote struct { + Version semver.Version Date time.Time Sections map[string]ReleaseNoteSection BreakingChanges []string diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index c65d063..674f772 100644 --- a/sv/releasenotes_test.go +++ b/sv/releasenotes_test.go @@ -4,41 +4,47 @@ import ( "reflect" "testing" "time" + + "github.com/Masterminds/semver" ) -func TestReleaseNoteProcessorImpl_Get(t *testing.T) { +func TestReleaseNoteProcessorImpl_Create(t *testing.T) { date := time.Now() tests := []struct { name string + version semver.Version date time.Time commits []GitCommitLog want ReleaseNote }{ { name: "mapped tag", + version: *semver.MustParse("1.0.0"), date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{})}, - want: releaseNote(date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), }, { name: "unmapped tag", + version: *semver.MustParse("1.0.0"), date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{})}, - want: releaseNote(date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), }, { name: "breaking changes tag", + version: *semver.MustParse("1.0.0"), date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breakingchange": "breaks"})}, - want: releaseNote(date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), + want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewReleaseNoteProcessor(map[string]string{"t1": "Tag 1", "t2": "Tag 2"}) - if got := p.Get(tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) { - t.Errorf("ReleaseNoteProcessorImpl.Get() = %v, want %v", got, tt.want) + if got := p.Create(tt.version, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want) } }) } From e18b0cc08f32fbea8016be26b6535132a1f10c5c Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sat, 1 Feb 2020 21:15:12 -0300 Subject: [PATCH 2/8] refactor: merge breaking change key and tag --- sv/git.go | 13 ++++++++----- sv/releasenotes.go | 2 +- sv/semver.go | 5 +---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sv/git.go b/sv/git.go index 709d336..b126b31 100644 --- a/sv/git.go +++ b/sv/git.go @@ -13,10 +13,13 @@ import ( ) const ( - logSeparator = "##" - endLine = "~~" - breakingChangesKey = "breakingchange" - issueIDKey = "issueid" + logSeparator = "##" + endLine = "~~" + + // BreakingChangesKey key to breaking change metadata + BreakingChangesKey = "breakingchange" + // IssueIDKey key to issue id metadata + IssueIDKey = "issueid" ) // Git commands @@ -52,7 +55,7 @@ type GitImpl struct { // NewGit constructor func NewGit(breakinChangePrefixes, issueIDPrefixes []string, tagPattern string) *GitImpl { return &GitImpl{ - messageMetadata: map[string][]string{breakingChangesKey: breakinChangePrefixes, issueIDKey: issueIDPrefixes}, + messageMetadata: map[string][]string{BreakingChangesKey: breakinChangePrefixes, IssueIDKey: issueIDPrefixes}, tagPattern: tagPattern, } } diff --git a/sv/releasenotes.go b/sv/releasenotes.go index 5389f1f..8b7ed08 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -62,7 +62,7 @@ func (p ReleaseNoteProcessorImpl) Create(version semver.Version, date time.Time, section.Items = append(section.Items, commit) sections[commit.Type] = section } - if value, exists := commit.Metadata[BreakingChangeTag]; exists { + if value, exists := commit.Metadata[BreakingChangesKey]; exists { breakingChanges = append(breakingChanges, value) } } diff --git a/sv/semver.go b/sv/semver.go index 1c0523c..8350bda 100644 --- a/sv/semver.go +++ b/sv/semver.go @@ -24,9 +24,6 @@ func ToVersion(value string) (semver.Version, error) { return *v, nil } -// BreakingChangeTag breaking change tag from commit metadata -const BreakingChangeTag string = "breakingchange" - // SemVerCommitsProcessor interface type SemVerCommitsProcessor interface { NextVersion(version semver.Version, commits []GitCommitLog) semver.Version @@ -72,7 +69,7 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version semver.Version, commits } func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType { - if _, exists := commit.Metadata[BreakingChangeTag]; exists { + if _, exists := commit.Metadata[BreakingChangesKey]; exists { return major } if _, exists := p.MajorVersionTypes[commit.Type]; exists { From f4d149933193186889b5a427153eb6bd74791761 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sat, 1 Feb 2020 22:40:31 -0300 Subject: [PATCH 3/8] refactor: extract template operation to formatter struct --- cmd/git-sv/handlers.go | 6 ++--- cmd/git-sv/main.go | 5 ++-- sv/formatter.go | 59 ++++++++++++++++++++++++++++++++++++++++++ sv/releasenotes.go | 46 ++------------------------------ 4 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 sv/formatter.go diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index f6da003..ebde62b 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -76,7 +76,7 @@ func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) { return git.Log(prev, tag) } -func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor) func(c *cli.Context) error { +func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter) func(c *cli.Context) error { return func(c *cli.Context) error { var commits []sv.GitCommitLog var rnVersion semver.Version @@ -94,7 +94,7 @@ func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, } releasenote := rnProcessor.Create(rnVersion, date, commits) - fmt.Println(rnProcessor.Format(releasenote)) + fmt.Println(outputFormatter.FormatReleaseNote(releasenote)) return nil } } @@ -161,7 +161,7 @@ func getNextVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) ( return semverProcessor.NextVersion(currentVer, commits), time.Now(), commits, nil } -func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor) func(c *cli.Context) error { +func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error { return func(c *cli.Context) error { describe := git.Describe() diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 67c8e20..2380f27 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -17,6 +17,7 @@ func main() { git := sv.NewGit(cfg.BreakingChangePrefixes, cfg.IssueIDPrefixes, cfg.TagPattern) semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags) + outputFormatter := sv.NewOutputFormatter() app := cli.NewApp() app.Name = "sv" @@ -46,14 +47,14 @@ func main() { Name: "release-notes", Aliases: []string{"rn"}, Usage: "generate release notes", - Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor), + Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), Flags: []cli.Flag{&cli.StringFlag{Name: "t", Usage: "get release note from tag"}}, }, { Name: "tag", Aliases: []string{"tg"}, Usage: "generate tag with version based on git commit messages", - Action: tagHandler(git, semverProcessor, releasenotesProcessor), + Action: tagHandler(git, semverProcessor), }, } diff --git a/sv/formatter.go b/sv/formatter.go new file mode 100644 index 0000000..b9d0d70 --- /dev/null +++ b/sv/formatter.go @@ -0,0 +1,59 @@ +package sv + +import ( + "bytes" + "fmt" + "text/template" +) + +type releaseNoteTemplateVariables struct { + Version string + Date string + Sections map[string]ReleaseNoteSection + BreakingChanges []string +} + +const rnTemplate = `## v{{.Version}} ({{.Date}}) + +{{if .Sections.feat}}### {{.Sections.feat.Name}} +{{range $k,$v := .Sections.feat.Items}} +- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} + +{{if .Sections.fix}}### {{.Sections.fix.Name}} +{{range $k,$v := .Sections.fix.Items}} +- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} + +{{if .BreakingChanges}}### Breaking Changes +{{range $k,$v := .BreakingChanges}} +- {{$v}}{{end}} +{{end}}` + +// OutputFormatter output formatter interface. +type OutputFormatter interface { + FormatReleaseNote(releasenote ReleaseNote) string +} + +// OutputFormatterImpl formater for release note and changelog. +type OutputFormatterImpl struct { + releasenoteTemplate *template.Template +} + +// NewOutputFormatter TemplateProcessor constructor. +func NewOutputFormatter() *OutputFormatterImpl { + template := template.Must(template.New("releasenotes").Parse(rnTemplate)) + return &OutputFormatterImpl{releasenoteTemplate: template} +} + +// FormatReleaseNote format a release note. +func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) string { + templateVars := releaseNoteTemplateVariables{ + Version: fmt.Sprintf("%d.%d.%d", releasenote.Version.Major(), releasenote.Version.Minor(), releasenote.Version.Patch()), + Date: releasenote.Date.Format("2006-01-02"), + Sections: releasenote.Sections, + BreakingChanges: releasenote.BreakingChanges, + } + + var b bytes.Buffer + p.releasenoteTemplate.Execute(&b, templateVars) + return b.String() +} diff --git a/sv/releasenotes.go b/sv/releasenotes.go index 8b7ed08..9a30f41 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -1,52 +1,24 @@ package sv import ( - "bytes" - "fmt" - "text/template" "time" "github.com/Masterminds/semver" ) -type releaseNoteTemplate struct { - Version string - Date string - Sections map[string]ReleaseNoteSection - BreakingChanges []string -} - -const markdownTemplate = `## v{{.Version}} ({{.Date}}) - -{{if .Sections.feat}}### {{.Sections.feat.Name}} -{{range $k,$v := .Sections.feat.Items}} -- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} - -{{if .Sections.fix}}### {{.Sections.fix.Name}} -{{range $k,$v := .Sections.fix.Items}} -- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} - -{{if .BreakingChanges}}### Breaking Changes -{{range $k,$v := .BreakingChanges}} -- {{$v}}{{end}} -{{end}}` - // ReleaseNoteProcessor release note processor interface. type ReleaseNoteProcessor interface { Create(version semver.Version, date time.Time, commits []GitCommitLog) ReleaseNote - Format(releasenote ReleaseNote) string } // ReleaseNoteProcessorImpl release note based on commit log. type ReleaseNoteProcessorImpl struct { - tags map[string]string - template *template.Template + tags map[string]string } // NewReleaseNoteProcessor ReleaseNoteProcessor constructor. func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl { - template := template.Must(template.New("markdown").Parse(markdownTemplate)) - return &ReleaseNoteProcessorImpl{tags: tags, template: template} + return &ReleaseNoteProcessorImpl{tags: tags} } // Create create a release note based on commits. @@ -70,20 +42,6 @@ func (p ReleaseNoteProcessorImpl) Create(version semver.Version, date time.Time, return ReleaseNote{Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges} } -// Format format a release note. -func (p ReleaseNoteProcessorImpl) Format(releasenote ReleaseNote) string { - templateVars := releaseNoteTemplate{ - Version: fmt.Sprintf("%d.%d.%d", releasenote.Version.Major(), releasenote.Version.Minor(), releasenote.Version.Patch()), - Date: releasenote.Date.Format("2006-01-02"), - Sections: releasenote.Sections, - BreakingChanges: releasenote.BreakingChanges, - } - - var b bytes.Buffer - p.template.Execute(&b, templateVars) - return b.String() -} - // ReleaseNote release note. type ReleaseNote struct { Version semver.Version From fef044c99def6f6ee6f506ec1ee620eac7ae90fc Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sat, 1 Feb 2020 23:05:39 -0300 Subject: [PATCH 4/8] fix: trim spaces from release notes template --- sv/formatter.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/sv/formatter.go b/sv/formatter.go index b9d0d70..8e9a195 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -14,19 +14,27 @@ type releaseNoteTemplateVariables struct { } const rnTemplate = `## v{{.Version}} ({{.Date}}) +{{- if .Sections.feat}} -{{if .Sections.feat}}### {{.Sections.feat.Name}} +### {{.Sections.feat.Name}} {{range $k,$v := .Sections.feat.Items}} -- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} +- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}){{if $v.Metadata.issueid}} ({{$v.Metadata.issueid}}){{end}}{{end}} +{{- end}} -{{if .Sections.fix}}### {{.Sections.fix.Name}} +{{- if .Sections.fix}} + +### {{.Sections.fix.Name}} {{range $k,$v := .Sections.fix.Items}} -- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} +- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}){{if $v.Metadata.issueid}} ({{$v.Metadata.issueid}}){{end}}{{end}} +{{- end}} + +{{- if .BreakingChanges}} -{{if .BreakingChanges}}### Breaking Changes +### Breaking Changes {{range $k,$v := .BreakingChanges}} - {{$v}}{{end}} -{{end}}` +{{- end}} +` // OutputFormatter output formatter interface. type OutputFormatter interface { From 851dcb91d206b8b5f4de1d58e9818326050497a4 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sat, 1 Feb 2020 23:51:29 -0300 Subject: [PATCH 5/8] refactor: split release notes template in sections --- sv/formatter.go | 37 +++++++++++++++++++------------------ sv/helpers_test.go | 2 +- sv/releasenotes_test.go | 6 +++--- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/sv/formatter.go b/sv/formatter.go index 8e9a195..ce33f16 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -13,27 +13,25 @@ type releaseNoteTemplateVariables struct { BreakingChanges []string } -const rnTemplate = `## v{{.Version}} ({{.Date}}) -{{- if .Sections.feat}} - -### {{.Sections.feat.Name}} -{{range $k,$v := .Sections.feat.Items}} -- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}){{if $v.Metadata.issueid}} ({{$v.Metadata.issueid}}){{end}}{{end}} -{{- end}} +const rnSectionItem = "- {{if .Scope}}**{{.Scope}}:** {{end}}{{.Subject}} ({{.Hash}}){{if .Metadata.issueid}} ({{.Metadata.issueid}}){{end}}" +const rnSection = `{{- if .}} -{{- if .Sections.fix}} - -### {{.Sections.fix.Name}} -{{range $k,$v := .Sections.fix.Items}} -- {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}){{if $v.Metadata.issueid}} ({{$v.Metadata.issueid}}){{end}}{{end}} +### {{.Name}} +{{range $k,$v := .Items}} +{{template "rnSectionItem" $v}} {{- end}} - -{{- if .BreakingChanges}} +{{- end}}` +const rnSectionBreakingChanges = `{{- if .}} ### Breaking Changes -{{range $k,$v := .BreakingChanges}} -- {{$v}}{{end}} +{{range $k,$v := .}} +- {{$v}} {{- end}} +{{- end}}` +const rnTemplate = `## v{{.Version}} ({{.Date}}) +{{- template "rnSection" .Sections.feat}} +{{- template "rnSection" .Sections.fix}} +{{- template "rnSectionBreakingChanges" .BreakingChanges}} ` // OutputFormatter output formatter interface. @@ -48,8 +46,11 @@ type OutputFormatterImpl struct { // NewOutputFormatter TemplateProcessor constructor. func NewOutputFormatter() *OutputFormatterImpl { - template := template.Must(template.New("releasenotes").Parse(rnTemplate)) - return &OutputFormatterImpl{releasenoteTemplate: template} + t := template.Must(template.New("releasenotes").Parse(rnTemplate)) + template.Must(t.New("rnSectionItem").Parse(rnSectionItem)) + template.Must(t.New("rnSection").Parse(rnSection)) + template.Must(t.New("rnSectionBreakingChanges").Parse(rnSectionBreakingChanges)) + return &OutputFormatterImpl{releasenoteTemplate: t} } // FormatReleaseNote format a release note. diff --git a/sv/helpers_test.go b/sv/helpers_test.go index d1c0273..69b6656 100644 --- a/sv/helpers_test.go +++ b/sv/helpers_test.go @@ -28,7 +28,7 @@ func releaseNote(version semver.Version, date time.Time, sections map[string]Rel } } -func rnSection(name string, items []GitCommitLog) ReleaseNoteSection { +func newReleaseNoteSection(name string, items []GitCommitLog) ReleaseNoteSection { return ReleaseNoteSection{ Name: name, Items: items, diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index 674f772..4ca20b6 100644 --- a/sv/releasenotes_test.go +++ b/sv/releasenotes_test.go @@ -23,21 +23,21 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version: *semver.MustParse("1.0.0"), date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{})}, - want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), }, { name: "unmapped tag", version: *semver.MustParse("1.0.0"), date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{})}, - want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), }, { name: "breaking changes tag", version: *semver.MustParse("1.0.0"), date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breakingchange": "breaks"})}, - want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), + want: releaseNote(*semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), }, } for _, tt := range tests { From 11e40b68ee61c439d61a9a711d3e27e2c42ceb7d Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 2 Feb 2020 00:27:03 -0300 Subject: [PATCH 6/8] refactor: add alias on release-notes and commit-log flags --- cmd/git-sv/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 2380f27..c4f1913 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -41,14 +41,14 @@ func main() { Aliases: []string{"cl"}, Usage: "list all commit logs since last version as jsons", Action: commitLogHandler(git, semverProcessor), - Flags: []cli.Flag{&cli.StringFlag{Name: "t", Usage: "get commit log from tag"}}, + Flags: []cli.Flag{&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get commit log from tag"}}, }, { Name: "release-notes", Aliases: []string{"rn"}, Usage: "generate release notes", Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), - Flags: []cli.Flag{&cli.StringFlag{Name: "t", Usage: "get release note from tag"}}, + Flags: []cli.Flag{&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get release note from tag"}}, }, { Name: "tag", From cc57ec4c76ad58278fe9849a9e35864f09458c0d Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 2 Feb 2020 00:49:54 -0300 Subject: [PATCH 7/8] feat: add changelog command --- README.md | 17 +++++++------ cmd/git-sv/handlers.go | 44 ++++++++++++++++++++++++++++++++ cmd/git-sv/main.go | 10 ++++++++ sv/formatter.go | 58 ++++++++++++++++++++++++++++++++---------- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8cf33a0..a70235a 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,15 @@ git-sv rn -h ##### Available commands -| Variable | description | -| --------- | ---------- | -| current-version, cv | get last released version from git | -| next-version, nv | generate the next version based on git commit messages | -| commit-log, cl | list all commit logs since last version as jsons | -| release-notes, rn | generate release notes | -| tag, tg | generate tag with version based on git commit messages | -| help, h | Shows a list of commands or help for one command | +| Variable | description | has options | +| --------- | ---------- | ---------- | +| current-version, cv | get last released version from git | :x: | +| next-version, nv | generate the next version based on git commit messages | :x: | +| commit-log, cl | list all commit logs since last version as jsons | :heavy_check_mark: | +| release-notes, rn | generate release notes | :heavy_check_mark: | +| changelog, cgl | generate changelog | :heavy_check_mark: | +| tag, tg | generate tag with version based on git commit messages | :x: | +| help, h | Shows a list of commands or help for one command | :x: | ## Development diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index ebde62b..6e2473c 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "sort" "sv4git/sv" "time" @@ -184,3 +185,46 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c return nil } } + +func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, formatter sv.OutputFormatter) func(c *cli.Context) error { + return func(c *cli.Context) error { + + tags, err := git.Tags() + if err != nil { + return err + } + sort.Slice(tags, func(i, j int) bool { + return tags[i].Date.After(tags[j].Date) + }) + + var releaseNotes []sv.ReleaseNote + + size := c.Int("size") + all := c.Bool("all") + for i, tag := range tags { + if !all && i >= size { + break + } + + previousTag := "" + if i+1 < len(tags) { + previousTag = tags[i+1].Name + } + + commits, err := git.Log(previousTag, tag.Name) + if err != nil { + return fmt.Errorf("error getting git log from tag: %s, message: %v", tag.Name, err) + } + + currentVer, err := sv.ToVersion(tag.Name) + if err != nil { + return fmt.Errorf("error parsing version: %s from describe, message: %v", tag.Name, err) + } + releaseNotes = append(releaseNotes, rnProcessor.Create(currentVer, tag.Date, commits)) + } + + fmt.Println(formatter.FormatChangelog(releaseNotes)) + + return nil + } +} diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index c4f1913..ebb1449 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -50,6 +50,16 @@ func main() { Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), Flags: []cli.Flag{&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get release note from tag"}}, }, + { + Name: "changelog", + Aliases: []string{"cgl"}, + Usage: "generate changelog", + Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), + Flags: []cli.Flag{ + &cli.IntFlag{Name: "size", Value: 10, Aliases: []string{"n"}, Usage: "get changelog from last 'n' tags"}, + &cli.BoolFlag{Name: "all", Usage: "ignore size parameter, get changelog for every tag"}, + }, + }, { Name: "tag", Aliases: []string{"tg"}, diff --git a/sv/formatter.go b/sv/formatter.go index ce33f16..74860db 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -13,56 +13,86 @@ type releaseNoteTemplateVariables struct { BreakingChanges []string } -const rnSectionItem = "- {{if .Scope}}**{{.Scope}}:** {{end}}{{.Subject}} ({{.Hash}}){{if .Metadata.issueid}} ({{.Metadata.issueid}}){{end}}" -const rnSection = `{{- if .}} +const ( + cglTemplate = `# Changelog +{{- range .}} + +{{template "rnTemplate" .}} +--- +{{- end}} +` + + rnSectionItem = "- {{if .Scope}}**{{.Scope}}:** {{end}}{{.Subject}} ({{.Hash}}){{if .Metadata.issueid}} ({{.Metadata.issueid}}){{end}}" + + rnSection = `{{- if .}} ### {{.Name}} {{range $k,$v := .Items}} {{template "rnSectionItem" $v}} {{- end}} {{- end}}` -const rnSectionBreakingChanges = `{{- if .}} + + rnSectionBreakingChanges = `{{- if .}} ### Breaking Changes {{range $k,$v := .}} - {{$v}} {{- end}} {{- end}}` -const rnTemplate = `## v{{.Version}} ({{.Date}}) + + rnTemplate = `## v{{.Version}} ({{.Date}}) {{- template "rnSection" .Sections.feat}} {{- template "rnSection" .Sections.fix}} {{- template "rnSectionBreakingChanges" .BreakingChanges}} ` +) // OutputFormatter output formatter interface. type OutputFormatter interface { FormatReleaseNote(releasenote ReleaseNote) string + FormatChangelog(releasenotes []ReleaseNote) string } // OutputFormatterImpl formater for release note and changelog. type OutputFormatterImpl struct { releasenoteTemplate *template.Template + changelogTemplate *template.Template } // NewOutputFormatter TemplateProcessor constructor. func NewOutputFormatter() *OutputFormatterImpl { - t := template.Must(template.New("releasenotes").Parse(rnTemplate)) - template.Must(t.New("rnSectionItem").Parse(rnSectionItem)) - template.Must(t.New("rnSection").Parse(rnSection)) - template.Must(t.New("rnSectionBreakingChanges").Parse(rnSectionBreakingChanges)) - return &OutputFormatterImpl{releasenoteTemplate: t} + cgl := template.Must(template.New("cglTemplate").Parse(cglTemplate)) + rn := template.Must(cgl.New("rnTemplate").Parse(rnTemplate)) + template.Must(rn.New("rnSectionItem").Parse(rnSectionItem)) + template.Must(rn.New("rnSection").Parse(rnSection)) + template.Must(rn.New("rnSectionBreakingChanges").Parse(rnSectionBreakingChanges)) + return &OutputFormatterImpl{releasenoteTemplate: rn, changelogTemplate: cgl} } // FormatReleaseNote format a release note. func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) string { - templateVars := releaseNoteTemplateVariables{ + var b bytes.Buffer + p.releasenoteTemplate.Execute(&b, releaseNoteVariables(releasenote)) + return b.String() +} + +// FormatChangelog format a changelog +func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) string { + var templateVars []releaseNoteTemplateVariables + for _, v := range releasenotes { + templateVars = append(templateVars, releaseNoteVariables(v)) + } + + var b bytes.Buffer + p.changelogTemplate.Execute(&b, templateVars) + return b.String() +} + +func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables { + return releaseNoteTemplateVariables{ Version: fmt.Sprintf("%d.%d.%d", releasenote.Version.Major(), releasenote.Version.Minor(), releasenote.Version.Patch()), Date: releasenote.Date.Format("2006-01-02"), Sections: releasenote.Sections, BreakingChanges: releasenote.BreakingChanges, } - - var b bytes.Buffer - p.releasenoteTemplate.Execute(&b, templateVars) - return b.String() } From d5a5da9c7f1261c66a662c54099171d7bded999e Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 2 Feb 2020 00:54:37 -0300 Subject: [PATCH 8/8] docs: center options on avaliable commands table --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a70235a..e820a02 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ git-sv rn -h ##### Available commands | Variable | description | has options | -| --------- | ---------- | ---------- | +| --------- | ---------- | :----------: | | current-version, cv | get last released version from git | :x: | | next-version, nv | generate the next version based on git commit messages | :x: | | commit-log, cl | list all commit logs since last version as jsons | :heavy_check_mark: |