diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index 9df2d16..4fbe133 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -77,8 +77,14 @@ func defaultConfig() Config { UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"}, IgnoreUnknown: false, }, - Tag: sv.TagConfig{Pattern: "%d.%d.%d"}, - ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"fix": "Bug Fixes", "feat": "Features", "breaking-change": "Breaking Changes"}}, + 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"}, + }, + }, Branches: sv.BranchesConfig{ Prefix: "([a-z]+\\/)?", Suffix: "(-.*)?", @@ -129,3 +135,35 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V } return nil } + +func migrateConfig(cfg Config) Config { + if cfg.ReleaseNotes.Headers == nil { + return cfg + } + warnf("config 'release-notes.headers' is deprecated, please use 'sections' instead!") + + return Config{ + Version: cfg.Version, + Versioning: cfg.Versioning, + Tag: cfg.Tag, + ReleaseNotes: sv.ReleaseNotesConfig{ + Sections: migrateReleaseNotesConfig(cfg.ReleaseNotes.Headers), + }, + Branches: cfg.Branches, + CommitMessage: cfg.CommitMessage, + } +} + +func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig { + order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"} + var sections []sv.ReleaseNotesSectionConfig + for _, key := range order { + if name, exists := headers[key]; exists { + sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{key}}) + } + } + if name, exists := headers["breaking-change"]; exists { + sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeBreakingChange}) + } + return sections +} diff --git a/sv/config.go b/sv/config.go index eb894e1..fda3506 100644 --- a/sv/config.go +++ b/sv/config.go @@ -68,5 +68,29 @@ type TagConfig struct { // ReleaseNotesConfig release notes preferences. type ReleaseNotesConfig struct { - Headers map[string]string `yaml:"headers"` + Headers map[string]string `yaml:"headers"` + Sections []ReleaseNotesSectionConfig `yaml:"sections"` } + +func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSectionConfig { + for _, sectionCfg := range cfg.Sections { + if sectionCfg.SectionType == sectionType { + return §ionCfg + } + } + return nil +} + +// ReleaseNotesSectionConfig preferences for a single section on release notes. +type ReleaseNotesSectionConfig struct { + Name string `yaml:"name"` + SectionType string `yaml:"section-type"` + CommitTypes []string `yaml:"commit-types"` +} + +const ( + // ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value. + ReleaseNotesSectionTypeCommits = "commits" + // ReleaseNotesSectionTypeBreakingChange ReleaseNotesSectionConfig.SectionType value. + ReleaseNotesSectionTypeBreakingChange = "breaking-change" +) diff --git a/sv/releasenotes.go b/sv/releasenotes.go index 0462df0..ab0cd7b 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -23,18 +23,20 @@ func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl { // Create create a release note based on commits. 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) authors := make(map[string]struct{}) var breakingChanges []string for _, commit := range commits { authors[commit.AuthorName] = struct{}{} - if name, exists := p.cfg.Headers[commit.Message.Type]; exists { - section, sexists := sections[commit.Message.Type] + if sectionCfg, exists := mapping[commit.Message.Type]; exists { + section, sexists := sections[sectionCfg.Name] if !sexists { - section = ReleaseNoteSection{Name: name, Types: []string{commit.Message.Type}} //TODO: change to support more than one type per section + section = ReleaseNoteSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes} } section.Items = append(section.Items, commit) - sections[commit.Message.Type] = section + sections[sectionCfg.Name] = section } if commit.Message.BreakingMessage() != "" { // TODO: if no message found, should use description instead? @@ -43,12 +45,24 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da } var breakingChangeSection BreakingChangeSection - if name, exists := p.cfg.Headers[breakingChangeMetadataKey]; exists && len(breakingChanges) > 0 { - breakingChangeSection = BreakingChangeSection{Name: name, Messages: breakingChanges} + if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChange); bcCfg != nil && len(breakingChanges) > 0 { + breakingChangeSection = BreakingChangeSection{Name: bcCfg.Name, Messages: breakingChanges} } return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChangeSection, AuthorsNames: authors} } +func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]ReleaseNotesSectionConfig { + mapping := make(map[string]ReleaseNotesSectionConfig) + for _, section := range sections { + if section.SectionType == ReleaseNotesSectionTypeCommits { + for _, commitType := range section.CommitTypes { + mapping[commitType] = section + } + } + } + return mapping +} + // ReleaseNote release note. type ReleaseNote struct { Version *semver.Version diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index 8cfcac8..43f6981 100644 --- a/sv/releasenotes_test.go +++ b/sv/releasenotes_test.go @@ -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{"t1": 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, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, nil, 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{"t1": 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, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, nil, 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{"t1": 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, map[string]ReleaseNoteSection{"Tag 1": newReleaseNoteSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, []string{"breaks"}, map[string]struct{}{"a": {}}), }, { name: "multiple authors", @@ -49,12 +49,14 @@ 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{"t1": 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, 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": {}}), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := NewReleaseNoteProcessor(ReleaseNotesConfig{Headers: map[string]string{"t1": "Tag 1", "t2": "Tag 2", "breaking-change": "Breaking Changes"}}) + 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"}}}) 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) }