0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-11-21 12:00:40 +00:00

feat: use release-notes.sections to sort sections on template variables

issue: #40
This commit is contained in:
Beatriz Vieira 2022-02-07 22:49:32 -03:00
parent e650f64783
commit ebb70048ee
11 changed files with 131 additions and 115 deletions

View File

@ -80,9 +80,9 @@ func defaultConfig() Config {
Tag: sv.TagConfig{Pattern: "%d.%d.%d"},
ReleaseNotes: sv.ReleaseNotesConfig{
Sections: []sv.ReleaseNotesSectionConfig{
{Name: "Features", SectionType: "commits", CommitTypes: []string{"feat"}},
{Name: "Bug Fixes", SectionType: "commits", CommitTypes: []string{"fix"}},
{Name: "Breaking Changes", SectionType: "breaking-change"},
{Name: "Features", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"feat"}},
{Name: "Bug Fixes", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"fix"}},
{Name: "Breaking Changes", SectionType: sv.ReleaseNotesSectionTypeBreakingChanges},
},
},
Branches: sv.BranchesConfig{
@ -163,7 +163,7 @@ func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSecti
}
}
if name, exists := headers["breaking-change"]; exists {
sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeBreakingChange})
sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeBreakingChanges})
}
return sections
}

View File

@ -1,5 +1,8 @@
## {{if .Release}}{{.Release}}{{end}}{{if and .Date .Release}} ({{end}}{{.Date}}{{if and .Date .Release}}){{end}}
## {{if .Release}}{{.Release}}{{end}}{{if and (not .Date.IsZero) .Release}} ({{end}}{{timefmt .Date "2006-01-02"}}{{if and (not .Date.IsZero) .Release}}){{end}}
{{- range $section := .Sections }}
{{- template "rn-md-section.tpl" $section }}
{{- if (eq $section.SectionType "commits") }}
{{- template "rn-md-section-commits.tpl" $section }}
{{- else if (eq $section.SectionType "breaking-changes")}}
{{- template "rn-md-section-breaking-changes.tpl" $section }}
{{- end}}
{{- end}}
{{- template "rn-md-section-breaking-changes.tpl" .BreakingChanges}}

View File

@ -0,0 +1,7 @@
{{- if .}}{{- if ne .SectionName ""}}
### {{.SectionName}}
{{range $k,$v := .Items}}
- {{if $v.Message.Scope}}**{{$v.Message.Scope}}:** {{end}}{{$v.Message.Description}} ({{$v.Hash}}){{if $v.Message.Metadata.issue}} ({{$v.Message.Metadata.issue}}){{end}}
{{- end}}
{{- end}}{{- end}}

View File

@ -1 +0,0 @@
- {{if .Message.Scope}}**{{.Message.Scope}}:** {{end}}{{.Message.Description}} ({{.Hash}}){{if .Message.Metadata.issue}} ({{.Message.Metadata.issue}}){{end}}

View File

@ -1,7 +0,0 @@
{{- if .}}{{- if ne .Name ""}}
### {{.Name}}
{{range $k,$v := .Items}}
{{template "rn-md-section-item.tpl" $v}}
{{- end}}
{{- end}}{{- end}}

View File

@ -91,6 +91,6 @@ type ReleaseNotesSectionConfig struct {
const (
// ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value.
ReleaseNotesSectionTypeCommits = "commits"
// ReleaseNotesSectionTypeBreakingChange ReleaseNotesSectionConfig.SectionType value.
ReleaseNotesSectionTypeBreakingChange = "breaking-change"
// ReleaseNotesSectionTypeBreakingChanges ReleaseNotesSectionConfig.SectionType value.
ReleaseNotesSectionTypeBreakingChanges = "breaking-changes"
)

View File

@ -4,19 +4,19 @@ import (
"bytes"
"io/fs"
"sort"
"strings"
"text/template"
"time"
"github.com/Masterminds/semver/v3"
)
type releaseNoteTemplateVariables struct {
Release string
Version *semver.Version
Date string
Sections []ReleaseNoteSection
BreakingChanges BreakingChangeSection
AuthorNames []string
Release string
Tag string
Version *semver.Version
Date time.Time
Sections []ReleaseNoteSection
AuthorNames []string
}
// OutputFormatter output formatter interface.
@ -32,7 +32,10 @@ type OutputFormatterImpl struct {
// NewOutputFormatter TemplateProcessor constructor.
func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl {
tpls := template.Must(template.New("templates").ParseFS(templatesFS, "*"))
templateFNs := map[string]interface{}{
"timefmt": timeFormat,
}
tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*"))
return &OutputFormatterImpl{templates: tpls}
}
@ -60,56 +63,20 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string
}
func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables {
date := ""
if !releasenote.Date.IsZero() {
date = releasenote.Date.Format("2006-01-02")
}
release := releasenote.Tag
if releasenote.Version != nil {
release = "v" + releasenote.Version.String()
}
return releaseNoteTemplateVariables{
Release: release,
Version: releasenote.Version,
Date: date,
Sections: toTemplateSections(releasenote.Sections),
BreakingChanges: releasenote.BreakingChanges,
AuthorNames: toSortedArray(releasenote.AuthorsNames),
Release: release,
Tag: releasenote.Tag,
Version: releasenote.Version,
Date: releasenote.Date,
Sections: releasenote.Sections,
AuthorNames: toSortedArray(releasenote.AuthorsNames),
}
}
func toTemplateSections(sections map[string]ReleaseNoteSection) []ReleaseNoteSection {
result := make([]ReleaseNoteSection, len(sections))
i := 0
for _, section := range sections {
result[i] = section
i++
}
order := map[string]int{"feat": 0, "fix": 1, "refactor": 2, "perf": 3, "test": 4, "build": 5, "ci": 6, "chore": 7, "docs": 8, "style": 9}
sort.SliceStable(result, func(i, j int) bool {
priority1, disambiguity1 := priority(result[i].Types, order)
priority2, disambiguity2 := priority(result[j].Types, order)
if priority1 == priority2 {
return disambiguity1 < disambiguity2
}
return priority1 < priority2
})
return result
}
func priority(types []string, order map[string]int) (int, string) {
sort.Strings(types)
p := -1
for _, t := range types {
if p == -1 || order[t] < p {
p = order[t]
}
}
return p, strings.Join(types, "-")
}
func toSortedArray(input map[string]struct{}) []string {
result := make([]string, len(input))
i := 0
@ -120,3 +87,10 @@ func toSortedArray(input map[string]struct{}) []string {
sort.Strings(result)
return result
}
func timeFormat(t time.Time, format string) string {
if t.IsZero() {
return ""
}
return t.Format(format)
}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"os"
"testing"
"text/template"
"time"
"github.com/Masterminds/semver/v3"
@ -83,16 +82,17 @@ func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
func fullReleaseNote(tag string, date time.Time) ReleaseNote {
v, _ := semver.NewVersion(tag)
sections := map[string]ReleaseNoteSection{
"build": newReleaseNoteSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
"feat": newReleaseNoteSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
"fix": newReleaseNoteSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
sections := []ReleaseNoteSection{
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
}
return releaseNote(v, tag, date, sections, []string{"break change message"}, map[string]struct{}{"a": {}})
return releaseNote(v, tag, date, sections, map[string]struct{}{"a": {}})
}
func Test_checkTemplatesExecution(t *testing.T) {
tpls := template.Must(template.New("templates").ParseFS(templatesFS, "*"))
tpls := NewOutputFormatter(templatesFS).templates
tests := []struct {
template string
variables interface{}
@ -118,13 +118,13 @@ func Test_checkTemplatesExecution(t *testing.T) {
func releaseNotesVariables(release string) releaseNoteTemplateVariables {
return releaseNoteTemplateVariables{
Release: release,
Date: "2006-01-02",
Date: time.Date(2006, 1, 02, 0, 0, 0, 0, time.UTC),
Sections: []ReleaseNoteSection{
newReleaseNoteSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
newReleaseNoteSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
newReleaseNoteSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
},
BreakingChanges: BreakingChangeSection{"Breaking Changes", []string{"break change message"}},
}
}

View File

@ -27,23 +27,18 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
}
}
func releaseNote(version *semver.Version, tag string, date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string, authorsNames map[string]struct{}) ReleaseNote {
var bchanges BreakingChangeSection
if len(breakingChanges) > 0 {
bchanges = BreakingChangeSection{Name: "Breaking Changes", Messages: breakingChanges}
}
func releaseNote(version *semver.Version, tag string, date time.Time, sections []ReleaseNoteSection, authorsNames map[string]struct{}) ReleaseNote {
return ReleaseNote{
Version: version,
Tag: tag,
Date: date.Truncate(time.Minute),
Sections: sections,
BreakingChanges: bchanges,
AuthorsNames: authorsNames,
Version: version,
Tag: tag,
Date: date.Truncate(time.Minute),
Sections: sections,
AuthorsNames: authorsNames,
}
}
func newReleaseNoteSection(name string, types []string, items []GitCommitLog) ReleaseNoteSection {
return ReleaseNoteSection{
func newReleaseNoteCommitsSection(name string, types []string, items []GitCommitLog) ReleaseNoteCommitsSection {
return ReleaseNoteCommitsSection{
Name: name,
Types: types,
Items: items,

View File

@ -25,7 +25,7 @@ func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote {
mapping := commitSectionMapping(p.cfg.Sections)
sections := make(map[string]ReleaseNoteSection)
sections := make(map[string]ReleaseNoteCommitsSection)
authors := make(map[string]struct{})
var breakingChanges []string
for _, commit := range commits {
@ -33,7 +33,7 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da
if sectionCfg, exists := mapping[commit.Message.Type]; exists {
section, sexists := sections[sectionCfg.Name]
if !sexists {
section = ReleaseNoteSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes}
section = ReleaseNoteCommitsSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes}
}
section.Items = append(section.Items, commit)
sections[sectionCfg.Name] = section
@ -44,11 +44,33 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da
}
}
var breakingChangeSection BreakingChangeSection
if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChange); bcCfg != nil && len(breakingChanges) > 0 {
breakingChangeSection = BreakingChangeSection{Name: bcCfg.Name, Messages: breakingChanges}
var breakingChangeSection ReleaseNoteBreakingChangeSection
if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChanges); bcCfg != nil && len(breakingChanges) > 0 {
breakingChangeSection = ReleaseNoteBreakingChangeSection{Name: bcCfg.Name, Messages: breakingChanges}
}
return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChangeSection, AuthorsNames: authors}
return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: p.toReleaseNoteSections(sections, breakingChangeSection), AuthorsNames: authors}
}
func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[string]ReleaseNoteCommitsSection, breakingChange ReleaseNoteBreakingChangeSection) []ReleaseNoteSection {
hasBreaking := 0
if breakingChange.Name != "" {
hasBreaking = 1
}
sections := make([]ReleaseNoteSection, len(commitSections)+hasBreaking)
i := 0
for _, cfg := range p.cfg.Sections {
if cfg.SectionType == ReleaseNotesSectionTypeBreakingChanges && hasBreaking > 0 {
sections[i] = breakingChange
i++
}
if s, exists := commitSections[cfg.Name]; cfg.SectionType == ReleaseNotesSectionTypeCommits && exists {
sections[i] = s
i++
}
}
return sections
}
func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]ReleaseNotesSectionConfig {
@ -65,23 +87,48 @@ func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]Relea
// ReleaseNote release note.
type ReleaseNote struct {
Version *semver.Version
Tag string
Date time.Time
Sections map[string]ReleaseNoteSection
BreakingChanges BreakingChangeSection
AuthorsNames map[string]struct{}
Version *semver.Version
Tag string
Date time.Time
Sections []ReleaseNoteSection
AuthorsNames map[string]struct{}
}
// BreakingChangeSection breaking change section.
type BreakingChangeSection struct {
// ReleaseNoteSection section in release notes.
type ReleaseNoteSection interface {
SectionType() string
SectionName() string
}
// ReleaseNoteBreakingChangeSection breaking change section.
type ReleaseNoteBreakingChangeSection struct {
Name string
Messages []string
}
// ReleaseNoteSection release note section.
type ReleaseNoteSection struct {
// SectionType section type.
func (ReleaseNoteBreakingChangeSection) SectionType() string {
return ReleaseNotesSectionTypeBreakingChanges
}
// SectionName section name.
func (s ReleaseNoteBreakingChangeSection) SectionName() string {
return s.Name
}
// ReleaseNoteCommitsSection release note section.
type ReleaseNoteCommitsSection struct {
Name string
Types []string
Items []GitCommitLog
}
// SectionType section type.
func (ReleaseNoteCommitsSection) SectionType() string {
return ReleaseNotesSectionTypeCommits
}
// SectionName section name.
func (s ReleaseNoteCommitsSection) SectionName() string {
return s.Name
}

View File

@ -25,7 +25,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, nil, map[string]struct{}{"a": {}}),
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, map[string]struct{}{"a": {}}),
},
{
name: "unmapped tag",
@ -33,7 +33,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, nil, map[string]struct{}{"a": {}}),
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, map[string]struct{}{"a": {}}),
},
{
name: "breaking changes tag",
@ -41,7 +41,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, []string{"breaks"}, map[string]struct{}{"a": {}}),
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}), ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}}}, map[string]struct{}{"a": {}}),
},
{
name: "multiple authors",
@ -49,14 +49,12 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")})}, nil, map[string]struct{}{"author1": {}, "author2": {}, "author3": {}}),
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")})}, map[string]struct{}{"author1": {}, "author2": {}, "author3": {}}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewReleaseNoteProcessor(ReleaseNotesConfig{Sections: []ReleaseNotesSectionConfig{{Name: "Tag 1", SectionType: "commits", CommitTypes: []string{"t1"}},
{Name: "Tag 2", SectionType: "commits", CommitTypes: []string{"t2"}},
{Name: "Breaking Changes", SectionType: "breaking-change"}}})
p := NewReleaseNoteProcessor(ReleaseNotesConfig{Sections: []ReleaseNotesSectionConfig{{Name: "Tag 1", SectionType: "commits", CommitTypes: []string{"t1"}}, {Name: "Tag 2", SectionType: "commits", CommitTypes: []string{"t2"}}, {Name: "Breaking Changes", SectionType: "breaking-changes"}}})
if got := p.Create(tt.version, tt.tag, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
}