From b5d9f9c535bb418aa0b65f4b4f9a5d5f4f7a95f8 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 30 Jan 2022 18:45:16 -0300 Subject: [PATCH 01/17] refactor: extract templates from code and use go:embed issue: #40 --- sv/formatter.go | 53 +++------------ sv/formatter_test.go | 68 +++++++++++++++++++ sv/resources/templates/changelog-md.tpl | 6 ++ sv/resources/templates/releasenotes-md.tpl | 6 ++ .../rn-md-section-breaking-changes.tpl | 7 ++ sv/resources/templates/rn-md-section-item.tpl | 1 + sv/resources/templates/rn-md-section.tpl | 7 ++ 7 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 sv/resources/templates/changelog-md.tpl create mode 100644 sv/resources/templates/releasenotes-md.tpl create mode 100644 sv/resources/templates/rn-md-section-breaking-changes.tpl create mode 100644 sv/resources/templates/rn-md-section-item.tpl create mode 100644 sv/resources/templates/rn-md-section.tpl diff --git a/sv/formatter.go b/sv/formatter.go index 9d93367..896de9f 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -2,6 +2,7 @@ package sv import ( "bytes" + "embed" "text/template" ) @@ -13,40 +14,9 @@ type releaseNoteTemplateVariables struct { BreakingChanges BreakingChangeSection } -const ( - cglTemplate = `# Changelog -{{- range .}} - -{{template "rnTemplate" .}} ---- -{{- end}} -` - - rnSectionItem = "- {{if .Message.Scope}}**{{.Message.Scope}}:** {{end}}{{.Message.Description}} ({{.Hash}}){{if .Message.Metadata.issue}} ({{.Message.Metadata.issue}}){{end}}" - - rnSection = `{{- if .}}{{- if ne .Name ""}} - -### {{.Name}} -{{range $k,$v := .Items}} -{{template "rnSectionItem" $v}} -{{- end}} -{{- end}}{{- end}}` - - rnSectionBreakingChanges = `{{- if ne .Name ""}} - -### {{.Name}} -{{range $k,$v := .Messages}} -- {{$v}} -{{- end}} -{{- end}}` - - rnTemplate = `## {{if .Release}}{{.Release}}{{end}}{{if and .Date .Release}} ({{end}}{{.Date}}{{if and .Date .Release}}){{end}} -{{- $sections := .Sections }} -{{- range $key := .Order }} -{{- template "rnSection" (index $sections $key) }} -{{- end}} -{{- template "rnSectionBreakingChanges" .BreakingChanges}} -` +var ( + //go:embed resources/templates/* + defaultTemplatesFS embed.FS ) // OutputFormatter output formatter interface. @@ -57,24 +27,19 @@ type OutputFormatter interface { // OutputFormatterImpl formater for release note and changelog. type OutputFormatterImpl struct { - releasenoteTemplate *template.Template - changelogTemplate *template.Template + templates *template.Template } // NewOutputFormatter TemplateProcessor constructor. func NewOutputFormatter() *OutputFormatterImpl { - 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} + tpls := template.Must(template.New("templates").ParseFS(defaultTemplatesFS, "resources/templates/*")) + return &OutputFormatterImpl{templates: tpls} } // FormatReleaseNote format a release note. func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string, error) { var b bytes.Buffer - if err := p.releasenoteTemplate.Execute(&b, releaseNoteVariables(releasenote)); err != nil { + if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil { return "", err } return b.String(), nil @@ -88,7 +53,7 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string } var b bytes.Buffer - if err := p.changelogTemplate.Execute(&b, templateVars); err != nil { + if err := p.templates.ExecuteTemplate(&b, "changelog-md.tpl", templateVars); err != nil { return "", err } return b.String(), nil diff --git a/sv/formatter_test.go b/sv/formatter_test.go index 73a5104..eabf4f5 100644 --- a/sv/formatter_test.go +++ b/sv/formatter_test.go @@ -1,7 +1,9 @@ package sv import ( + "bytes" "testing" + "text/template" "time" "github.com/Masterminds/semver/v3" @@ -85,3 +87,69 @@ func fullReleaseNote(tag string, date time.Time) ReleaseNote { } return releaseNote(v, tag, date, sections, []string{"break change message"}) } + +func releaseNotesVariables(release string) releaseNoteTemplateVariables { + return releaseNoteTemplateVariables{ + Release: release, + Date: "2006-01-02", + Sections: map[string]ReleaseNoteSection{ + "build": newReleaseNoteSection("Build", []GitCommitLog{commitlog("build", map[string]string{})}), + "feat": newReleaseNoteSection("Features", []GitCommitLog{commitlog("feat", map[string]string{})}), + "fix": newReleaseNoteSection("Bug Fixes", []GitCommitLog{commitlog("fix", map[string]string{})}), + }, + Order: []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}, + BreakingChanges: BreakingChangeSection{"Breaking Changes", []string{"break change message"}}, + } +} + +func changelogVariables(releases ...string) []releaseNoteTemplateVariables { + var variables []releaseNoteTemplateVariables + for _, r := range releases { + variables = append(variables, releaseNotesVariables(r)) + } + return variables + +} + +func Test_checkTemplatesFiles(t *testing.T) { + tests := []string{ + "resources/templates/changelog-md.tpl", + "resources/templates/releasenotes-md.tpl", + } + for _, tt := range tests { + t.Run(tt, func(t *testing.T) { + got, err := defaultTemplatesFS.ReadFile(tt) + if err != nil { + t.Errorf("missing template error = %v", err) + return + } + if len(got) <= 0 { + t.Errorf("empty template") + } + }) + } +} + +func Test_checkTemplatesExecution(t *testing.T) { + tpls := template.Must(template.New("templates").ParseFS(defaultTemplatesFS, "resources/templates/*")) + tests := []struct { + template string + variables interface{} + }{ + {"changelog-md.tpl", changelogVariables("v1.0.0", "v1.0.1")}, + {"releasenotes-md.tpl", releaseNotesVariables("v1.0.0")}, + } + for _, tt := range tests { + t.Run(tt.template, func(t *testing.T) { + var b bytes.Buffer + err := tpls.ExecuteTemplate(&b, tt.template, tt.variables) + if err != nil { + t.Errorf("invalid template err = %v", err) + return + } + if len(b.Bytes()) <= 0 { + t.Errorf("empty template") + } + }) + } +} diff --git a/sv/resources/templates/changelog-md.tpl b/sv/resources/templates/changelog-md.tpl new file mode 100644 index 0000000..d478272 --- /dev/null +++ b/sv/resources/templates/changelog-md.tpl @@ -0,0 +1,6 @@ +# Changelog +{{- range .}} + +{{template "releasenotes-md.tpl" .}} +--- +{{- end}} \ No newline at end of file diff --git a/sv/resources/templates/releasenotes-md.tpl b/sv/resources/templates/releasenotes-md.tpl new file mode 100644 index 0000000..440cb6b --- /dev/null +++ b/sv/resources/templates/releasenotes-md.tpl @@ -0,0 +1,6 @@ +## {{if .Release}}{{.Release}}{{end}}{{if and .Date .Release}} ({{end}}{{.Date}}{{if and .Date .Release}}){{end}} +{{- $sections := .Sections }} +{{- range $key := .Order }} +{{- template "rn-md-section.tpl" (index $sections $key) }} +{{- end}} +{{- template "rn-md-section-breaking-changes.tpl" .BreakingChanges}} diff --git a/sv/resources/templates/rn-md-section-breaking-changes.tpl b/sv/resources/templates/rn-md-section-breaking-changes.tpl new file mode 100644 index 0000000..862b1dc --- /dev/null +++ b/sv/resources/templates/rn-md-section-breaking-changes.tpl @@ -0,0 +1,7 @@ +{{- if ne .Name ""}} + +### {{.Name}} +{{range $k,$v := .Messages}} +- {{$v}} +{{- end}} +{{- end}} \ No newline at end of file diff --git a/sv/resources/templates/rn-md-section-item.tpl b/sv/resources/templates/rn-md-section-item.tpl new file mode 100644 index 0000000..f0783a3 --- /dev/null +++ b/sv/resources/templates/rn-md-section-item.tpl @@ -0,0 +1 @@ +- {{if .Message.Scope}}**{{.Message.Scope}}:** {{end}}{{.Message.Description}} ({{.Hash}}){{if .Message.Metadata.issue}} ({{.Message.Metadata.issue}}){{end}} \ No newline at end of file diff --git a/sv/resources/templates/rn-md-section.tpl b/sv/resources/templates/rn-md-section.tpl new file mode 100644 index 0000000..32c7759 --- /dev/null +++ b/sv/resources/templates/rn-md-section.tpl @@ -0,0 +1,7 @@ +{{- if .}}{{- if ne .Name ""}} + +### {{.Name}} +{{range $k,$v := .Items}} +{{template "rn-md-section-item.tpl" $v}} +{{- end}} +{{- end}}{{- end}} \ No newline at end of file From 0230b1e00ff8f05ab03ab8b8ad80660119917e75 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 6 Feb 2022 18:15:36 -0300 Subject: [PATCH 02/17] refactor: move templates from sv to cmd/git-sv issue: #40 --- cmd/git-sv/main.go | 19 +++-- .../resources/templates/changelog-md.tpl | 0 .../resources/templates/releasenotes-md.tpl | 0 .../rn-md-section-breaking-changes.tpl | 0 .../templates/rn-md-section-item.tpl | 0 .../resources/templates/rn-md-section.tpl | 0 cmd/git-sv/resources_test.go | 24 +++++++ sv/formatter.go | 11 +-- sv/formatter_test.go | 72 ++++++++----------- 9 files changed, 70 insertions(+), 56 deletions(-) rename {sv => cmd/git-sv}/resources/templates/changelog-md.tpl (100%) rename {sv => cmd/git-sv}/resources/templates/releasenotes-md.tpl (100%) rename {sv => cmd/git-sv}/resources/templates/rn-md-section-breaking-changes.tpl (100%) rename {sv => cmd/git-sv}/resources/templates/rn-md-section-item.tpl (100%) rename {sv => cmd/git-sv}/resources/templates/rn-md-section.tpl (100%) create mode 100644 cmd/git-sv/resources_test.go diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index fa2053f..806d4b2 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -1,6 +1,8 @@ package main import ( + "embed" + "io/fs" "log" "os" "path/filepath" @@ -17,6 +19,16 @@ const ( repoConfigFilename = ".sv4git.yml" ) +var ( + //go:embed resources/templates/*.tpl + defaultTemplatesFS embed.FS +) + +func templateFS() fs.FS { + defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates") + return defaultTemplatesFS +} + func main() { log.SetFlags(0) @@ -25,7 +37,7 @@ func main() { git := sv.NewGit(messageProcessor, cfg.Tag) semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes) - outputFormatter := sv.NewOutputFormatter() + outputFormatter := sv.NewOutputFormatter(templateFS()) app := cli.NewApp() app.Name = "sv" @@ -146,10 +158,9 @@ func main() { } func loadCfg() Config { - envCfg := loadEnvConfig() - cfg := defaultConfig() + envCfg := loadEnvConfig() if envCfg.Home != "" { if homeCfg, err := readConfig(filepath.Join(envCfg.Home, configFilename)); err == nil { if merr := merge(&cfg, homeCfg); merr != nil { @@ -160,7 +171,7 @@ func loadCfg() Config { repoPath, rerr := getRepoPath() if rerr != nil { - log.Fatal("failed to get repository path, error: ", rerr) + log.Fatal("failed to discovery repository top level, error: ", rerr) } if repoCfg, err := readConfig(filepath.Join(repoPath, repoConfigFilename)); err == nil { diff --git a/sv/resources/templates/changelog-md.tpl b/cmd/git-sv/resources/templates/changelog-md.tpl similarity index 100% rename from sv/resources/templates/changelog-md.tpl rename to cmd/git-sv/resources/templates/changelog-md.tpl diff --git a/sv/resources/templates/releasenotes-md.tpl b/cmd/git-sv/resources/templates/releasenotes-md.tpl similarity index 100% rename from sv/resources/templates/releasenotes-md.tpl rename to cmd/git-sv/resources/templates/releasenotes-md.tpl diff --git a/sv/resources/templates/rn-md-section-breaking-changes.tpl b/cmd/git-sv/resources/templates/rn-md-section-breaking-changes.tpl similarity index 100% rename from sv/resources/templates/rn-md-section-breaking-changes.tpl rename to cmd/git-sv/resources/templates/rn-md-section-breaking-changes.tpl diff --git a/sv/resources/templates/rn-md-section-item.tpl b/cmd/git-sv/resources/templates/rn-md-section-item.tpl similarity index 100% rename from sv/resources/templates/rn-md-section-item.tpl rename to cmd/git-sv/resources/templates/rn-md-section-item.tpl diff --git a/sv/resources/templates/rn-md-section.tpl b/cmd/git-sv/resources/templates/rn-md-section.tpl similarity index 100% rename from sv/resources/templates/rn-md-section.tpl rename to cmd/git-sv/resources/templates/rn-md-section.tpl diff --git a/cmd/git-sv/resources_test.go b/cmd/git-sv/resources_test.go new file mode 100644 index 0000000..4d22540 --- /dev/null +++ b/cmd/git-sv/resources_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "testing" +) + +func Test_checkTemplatesFiles(t *testing.T) { + tests := []string{ + "resources/templates/changelog-md.tpl", + "resources/templates/releasenotes-md.tpl", + } + for _, tt := range tests { + t.Run(tt, func(t *testing.T) { + got, err := defaultTemplatesFS.ReadFile(tt) + if err != nil { + t.Errorf("missing template error = %v", err) + return + } + if len(got) <= 0 { + t.Errorf("empty template") + } + }) + } +} diff --git a/sv/formatter.go b/sv/formatter.go index 896de9f..f47d345 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -2,7 +2,7 @@ package sv import ( "bytes" - "embed" + "io/fs" "text/template" ) @@ -14,11 +14,6 @@ type releaseNoteTemplateVariables struct { BreakingChanges BreakingChangeSection } -var ( - //go:embed resources/templates/* - defaultTemplatesFS embed.FS -) - // OutputFormatter output formatter interface. type OutputFormatter interface { FormatReleaseNote(releasenote ReleaseNote) (string, error) @@ -31,8 +26,8 @@ type OutputFormatterImpl struct { } // NewOutputFormatter TemplateProcessor constructor. -func NewOutputFormatter() *OutputFormatterImpl { - tpls := template.Must(template.New("templates").ParseFS(defaultTemplatesFS, "resources/templates/*")) +func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl { + tpls := template.Must(template.New("templates").ParseFS(templatesFS, "*")) return &OutputFormatterImpl{templates: tpls} } diff --git a/sv/formatter_test.go b/sv/formatter_test.go index eabf4f5..e186548 100644 --- a/sv/formatter_test.go +++ b/sv/formatter_test.go @@ -2,6 +2,7 @@ package sv import ( "bytes" + "os" "testing" "text/template" "time" @@ -9,6 +10,8 @@ import ( "github.com/Masterminds/semver/v3" ) +var templatesFS = os.DirFS("../cmd/git-sv/resources/templates") + var dateChangelog = `## v1.0.0 (2020-05-01) ` @@ -57,7 +60,7 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewOutputFormatter().FormatReleaseNote(tt.input) + got, err := NewOutputFormatter(templatesFS).FormatReleaseNote(tt.input) if got != tt.want { t.Errorf("OutputFormatterImpl.FormatReleaseNote() = %v, want %v", got, tt.want) } @@ -88,6 +91,30 @@ func fullReleaseNote(tag string, date time.Time) ReleaseNote { return releaseNote(v, tag, date, sections, []string{"break change message"}) } +func Test_checkTemplatesExecution(t *testing.T) { + tpls := template.Must(template.New("templates").ParseFS(templatesFS, "*")) + tests := []struct { + template string + variables interface{} + }{ + {"changelog-md.tpl", changelogVariables("v1.0.0", "v1.0.1")}, + {"releasenotes-md.tpl", releaseNotesVariables("v1.0.0")}, + } + for _, tt := range tests { + t.Run(tt.template, func(t *testing.T) { + var b bytes.Buffer + err := tpls.ExecuteTemplate(&b, tt.template, tt.variables) + if err != nil { + t.Errorf("invalid template err = %v", err) + return + } + if len(b.Bytes()) <= 0 { + t.Errorf("empty template") + } + }) + } +} + func releaseNotesVariables(release string) releaseNoteTemplateVariables { return releaseNoteTemplateVariables{ Release: release, @@ -110,46 +137,3 @@ func changelogVariables(releases ...string) []releaseNoteTemplateVariables { return variables } - -func Test_checkTemplatesFiles(t *testing.T) { - tests := []string{ - "resources/templates/changelog-md.tpl", - "resources/templates/releasenotes-md.tpl", - } - for _, tt := range tests { - t.Run(tt, func(t *testing.T) { - got, err := defaultTemplatesFS.ReadFile(tt) - if err != nil { - t.Errorf("missing template error = %v", err) - return - } - if len(got) <= 0 { - t.Errorf("empty template") - } - }) - } -} - -func Test_checkTemplatesExecution(t *testing.T) { - tpls := template.Must(template.New("templates").ParseFS(defaultTemplatesFS, "resources/templates/*")) - tests := []struct { - template string - variables interface{} - }{ - {"changelog-md.tpl", changelogVariables("v1.0.0", "v1.0.1")}, - {"releasenotes-md.tpl", releaseNotesVariables("v1.0.0")}, - } - for _, tt := range tests { - t.Run(tt.template, func(t *testing.T) { - var b bytes.Buffer - err := tpls.ExecuteTemplate(&b, tt.template, tt.variables) - if err != nil { - t.Errorf("invalid template err = %v", err) - return - } - if len(b.Bytes()) <= 0 { - t.Errorf("empty template") - } - }) - } -} From ef2bc8638a84f082f169ae7c4862dcbaec844e20 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 6 Feb 2022 18:22:54 -0300 Subject: [PATCH 03/17] feat: add support to use templates defined on the repository (.sv4git/templates) issue: #40 --- cmd/git-sv/main.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 806d4b2..6ecc1bf 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -17,6 +17,7 @@ var Version = "source" const ( configFilename = "config.yml" repoConfigFilename = ".sv4git.yml" + configDir = ".sv4git" ) var ( @@ -24,20 +25,28 @@ var ( defaultTemplatesFS embed.FS ) -func templateFS() fs.FS { - defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates") - return defaultTemplatesFS +func templateFS(filepath string) fs.FS { + if _, err := os.Stat(filepath); err != nil { + defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates") + return defaultTemplatesFS + } + return os.DirFS(filepath) } func main() { log.SetFlags(0) - cfg := loadCfg() + repoPath, rerr := getRepoPath() + if rerr != nil { + log.Fatal("failed to discovery repository top level, error: ", rerr) + } + + cfg := loadCfg(repoPath) messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches) git := sv.NewGit(messageProcessor, cfg.Tag) semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes) - outputFormatter := sv.NewOutputFormatter(templateFS()) + outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(repoPath, configDir, "templates"))) app := cli.NewApp() app.Name = "sv" @@ -157,7 +166,7 @@ func main() { } } -func loadCfg() Config { +func loadCfg(repoPath string) Config { cfg := defaultConfig() envCfg := loadEnvConfig() @@ -169,11 +178,6 @@ func loadCfg() Config { } } - repoPath, rerr := getRepoPath() - if rerr != nil { - log.Fatal("failed to discovery repository top level, error: ", rerr) - } - if repoCfg, err := readConfig(filepath.Join(repoPath, repoConfigFilename)); err == nil { if merr := merge(&cfg, repoCfg); merr != nil { log.Fatal("failed to merge repo config, error: ", merr) From 243d73b5c2f7c4d572067a96262ffe1d14541f05 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 6 Feb 2022 20:16:52 -0300 Subject: [PATCH 04/17] feat: add authorName and timestamp to git commit issue: #40 --- sv/git.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/sv/git.go b/sv/git.go index bab6d14..7474824 100644 --- a/sv/git.go +++ b/sv/git.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "strconv" "strings" "time" @@ -31,9 +32,11 @@ type Git interface { // GitCommitLog description of a single commit log. type GitCommitLog struct { - Date string `json:"date,omitempty"` - Hash string `json:"hash,omitempty"` - Message CommitMessage `json:"message,omitempty"` + Date string `json:"date,omitempty"` + Timestamp int `json:"timestamp,omitempty"` + AuthorName string `json:"authorName,omitempty"` + Hash string `json:"hash,omitempty"` + Message CommitMessage `json:"message,omitempty"` } // GitTag git tag info. @@ -90,7 +93,7 @@ func (GitImpl) LastTag() string { // Log return git log. func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) { - format := "--pretty=format:\"%ad" + logSeparator + "%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\"" + format := "--pretty=format:\"%ad" + logSeparator + "%at" + logSeparator + "%cN" + logSeparator + "%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\"" params := []string{"log", "--date=short", format} if lr.start != "" || lr.end != "" { @@ -200,10 +203,13 @@ func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLo func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog { content := strings.Split(strings.Trim(commit, "\""), logSeparator) + timestamp, _ := strconv.Atoi(content[1]) return GitCommitLog{ - Date: content[0], - Hash: content[1], - Message: messageProcessor.Parse(content[2], content[3]), + Date: content[0], + Timestamp: timestamp, + AuthorName: content[2], + Hash: content[3], + Message: messageProcessor.Parse(content[4], content[5]), } } From 94fff067a18d73d3ecdd50df1cb1354f07813ab1 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 6 Feb 2022 22:06:46 -0300 Subject: [PATCH 05/17] refactor: remove order from templates variables and add authors issue: #40 --- .../resources/templates/releasenotes-md.tpl | 5 +- sv/formatter.go | 60 ++++++++++++++++--- sv/formatter_test.go | 17 +++--- sv/helpers_test.go | 9 ++- sv/releasenotes.go | 8 ++- sv/releasenotes_test.go | 20 +++++-- sv/semver_test.go | 16 ++--- 7 files changed, 97 insertions(+), 38 deletions(-) diff --git a/cmd/git-sv/resources/templates/releasenotes-md.tpl b/cmd/git-sv/resources/templates/releasenotes-md.tpl index 440cb6b..f6ba595 100644 --- a/cmd/git-sv/resources/templates/releasenotes-md.tpl +++ b/cmd/git-sv/resources/templates/releasenotes-md.tpl @@ -1,6 +1,5 @@ ## {{if .Release}}{{.Release}}{{end}}{{if and .Date .Release}} ({{end}}{{.Date}}{{if and .Date .Release}}){{end}} -{{- $sections := .Sections }} -{{- range $key := .Order }} -{{- template "rn-md-section.tpl" (index $sections $key) }} +{{- range $section := .Sections }} +{{- template "rn-md-section.tpl" $section }} {{- end}} {{- template "rn-md-section-breaking-changes.tpl" .BreakingChanges}} diff --git a/sv/formatter.go b/sv/formatter.go index f47d345..80fa2d1 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -3,15 +3,20 @@ package sv import ( "bytes" "io/fs" + "sort" + "strings" "text/template" + + "github.com/Masterminds/semver/v3" ) type releaseNoteTemplateVariables struct { Release string + Version *semver.Version Date string - Sections map[string]ReleaseNoteSection - Order []string + Sections []ReleaseNoteSection BreakingChanges BreakingChangeSection + AuthorNames []string } // OutputFormatter output formatter interface. @@ -60,17 +65,58 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables date = releasenote.Date.Format("2006-01-02") } - release := "" + release := releasenote.Tag if releasenote.Version != nil { release = "v" + releasenote.Version.String() - } else if releasenote.Tag != "" { - release = releasenote.Tag } return releaseNoteTemplateVariables{ Release: release, + Version: releasenote.Version, Date: date, - Sections: releasenote.Sections, - Order: []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}, + Sections: toTemplateSections(releasenote.Sections), BreakingChanges: releasenote.BreakingChanges, + 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 + for k := range input { + result[i] = k + i++ + } + sort.Strings(result) + return result +} diff --git a/sv/formatter_test.go b/sv/formatter_test.go index e186548..05acb30 100644 --- a/sv/formatter_test.go +++ b/sv/formatter_test.go @@ -84,11 +84,11 @@ 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", []GitCommitLog{commitlog("build", map[string]string{})}), - "feat": newReleaseNoteSection("Features", []GitCommitLog{commitlog("feat", map[string]string{})}), - "fix": newReleaseNoteSection("Bug Fixes", []GitCommitLog{commitlog("fix", map[string]string{})}), + "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")}), } - return releaseNote(v, tag, date, sections, []string{"break change message"}) + return releaseNote(v, tag, date, sections, []string{"break change message"}, map[string]struct{}{"a": {}}) } func Test_checkTemplatesExecution(t *testing.T) { @@ -119,12 +119,11 @@ func releaseNotesVariables(release string) releaseNoteTemplateVariables { return releaseNoteTemplateVariables{ Release: release, Date: "2006-01-02", - Sections: map[string]ReleaseNoteSection{ - "build": newReleaseNoteSection("Build", []GitCommitLog{commitlog("build", map[string]string{})}), - "feat": newReleaseNoteSection("Features", []GitCommitLog{commitlog("feat", map[string]string{})}), - "fix": newReleaseNoteSection("Bug Fixes", []GitCommitLog{commitlog("fix", map[string]string{})}), + 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")}), }, - Order: []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}, BreakingChanges: BreakingChangeSection{"Breaking Changes", []string{"break change message"}}, } } diff --git a/sv/helpers_test.go b/sv/helpers_test.go index 90975dd..5b17f4d 100644 --- a/sv/helpers_test.go +++ b/sv/helpers_test.go @@ -11,7 +11,7 @@ func version(v string) *semver.Version { return r } -func commitlog(ctype string, metadata map[string]string) GitCommitLog { +func commitlog(ctype string, metadata map[string]string, author string) GitCommitLog { breaking := false if _, found := metadata[breakingChangeMetadataKey]; found { breaking = true @@ -23,10 +23,11 @@ func commitlog(ctype string, metadata map[string]string) GitCommitLog { IsBreakingChange: breaking, Metadata: metadata, }, + AuthorName: author, } } -func releaseNote(version *semver.Version, tag string, date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote { +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} @@ -37,12 +38,14 @@ func releaseNote(version *semver.Version, tag string, date time.Time, sections m Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: bchanges, + AuthorsNames: authorsNames, } } -func newReleaseNoteSection(name string, items []GitCommitLog) ReleaseNoteSection { +func newReleaseNoteSection(name string, types []string, items []GitCommitLog) ReleaseNoteSection { return ReleaseNoteSection{ Name: name, + Types: types, Items: items, } } diff --git a/sv/releasenotes.go b/sv/releasenotes.go index 3c44ba0..0462df0 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -24,12 +24,14 @@ 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 { 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 !sexists { - section = ReleaseNoteSection{Name: name} + section = ReleaseNoteSection{Name: name, Types: []string{commit.Message.Type}} //TODO: change to support more than one type per section } section.Items = append(section.Items, commit) sections[commit.Message.Type] = section @@ -44,7 +46,7 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da if name, exists := p.cfg.Headers[breakingChangeMetadataKey]; exists && len(breakingChanges) > 0 { breakingChangeSection = BreakingChangeSection{Name: name, Messages: breakingChanges} } - return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChangeSection} + return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChangeSection, AuthorsNames: authors} } // ReleaseNote release note. @@ -54,6 +56,7 @@ type ReleaseNote struct { Date time.Time Sections map[string]ReleaseNoteSection BreakingChanges BreakingChangeSection + AuthorsNames map[string]struct{} } // BreakingChangeSection breaking change section. @@ -65,5 +68,6 @@ type BreakingChangeSection struct { // ReleaseNoteSection release note section. type ReleaseNoteSection struct { Name string + Types []string Items []GitCommitLog } diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index 7af1578..8cfcac8 100644 --- a/sv/releasenotes_test.go +++ b/sv/releasenotes_test.go @@ -24,24 +24,32 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{commitlog("t1", map[string]string{})}, - want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + 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": {}}), }, { name: "unmapped tag", version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{})}, - want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + 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": {}}), }, { name: "breaking changes tag", version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breaking-change": "breaks"})}, - want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), + 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": {}}), + }, + { + name: "multiple authors", + version: semver.MustParse("1.0.0"), + 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": {}}), }, } for _, tt := range tests { diff --git a/sv/semver_test.go b/sv/semver_test.go index 0f2a5f4..1940a39 100644 --- a/sv/semver_test.go +++ b/sv/semver_test.go @@ -18,14 +18,14 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) { }{ {"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0"), false}, {"no update without version", true, nil, []GitCommitLog{}, nil, false}, - {"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0"), false}, - {"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{})}, version("0.0.0"), false}, - {"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1"), true}, - {"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1"), true}, - {"patch update without version", false, nil, []GitCommitLog{commitlog("patch", map[string]string{})}, nil, true}, - {"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0"), true}, - {"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0"), true}, - {"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("patch", map[string]string{"breaking-change": "break"})}, version("1.0.0"), true}, + {"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.0"), false}, + {"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{}, "a")}, version("0.0.0"), false}, + {"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.1"), true}, + {"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, version("0.0.1"), true}, + {"patch update without version", false, nil, []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, nil, true}, + {"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("minor", map[string]string{}, "a")}, version("0.1.0"), true}, + {"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("major", map[string]string{}, "a")}, version("1.0.0"), true}, + {"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("patch", map[string]string{"breaking-change": "break"}, "a")}, version("1.0.0"), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From e64d4ddcc13178cbf842f020aa179430e8d493a4 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 7 Feb 2022 20:37:39 -0300 Subject: [PATCH 06/17] feat: print warning logs on stderr issue: #40 --- cmd/git-sv/log.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/git-sv/log.go b/cmd/git-sv/log.go index 69b0feb..d5a57c5 100644 --- a/cmd/git-sv/log.go +++ b/cmd/git-sv/log.go @@ -1,7 +1,10 @@ package main -import "fmt" +import ( + "fmt" + "os" +) func warnf(format string, values ...interface{}) { - fmt.Printf("WARN: "+format+"\n", values...) + fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", values...) } From e650f64783a4e50b0f29dca526f1a205fcb43830 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 7 Feb 2022 20:40:53 -0300 Subject: [PATCH 07/17] feat: add sections to release-notes config (headers is deprecated) issue: #40 --- cmd/git-sv/config.go | 42 +++++++++++++++++++++++++++++++++++++++-- sv/config.go | 26 ++++++++++++++++++++++++- sv/releasenotes.go | 26 +++++++++++++++++++------ sv/releasenotes_test.go | 12 +++++++----- 4 files changed, 92 insertions(+), 14 deletions(-) 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) } From ebb70048ee4ac95078ee656f6ff4fb2ed8b15c71 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 7 Feb 2022 22:49:32 -0300 Subject: [PATCH 08/17] feat: use release-notes.sections to sort sections on template variables issue: #40 --- cmd/git-sv/config.go | 8 +- .../resources/templates/releasenotes-md.tpl | 9 ++- .../templates/rn-md-section-commits.tpl | 7 ++ .../templates/rn-md-section-item.tpl | 1 - .../resources/templates/rn-md-section.tpl | 7 -- sv/config.go | 4 +- sv/formatter.go | 74 ++++++----------- sv/formatter_test.go | 24 +++--- sv/helpers_test.go | 21 ++--- sv/releasenotes.go | 79 +++++++++++++++---- sv/releasenotes_test.go | 12 ++- 11 files changed, 131 insertions(+), 115 deletions(-) create mode 100644 cmd/git-sv/resources/templates/rn-md-section-commits.tpl delete mode 100644 cmd/git-sv/resources/templates/rn-md-section-item.tpl delete mode 100644 cmd/git-sv/resources/templates/rn-md-section.tpl diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index 4fbe133..7e84068 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -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 } diff --git a/cmd/git-sv/resources/templates/releasenotes-md.tpl b/cmd/git-sv/resources/templates/releasenotes-md.tpl index f6ba595..a513e69 100644 --- a/cmd/git-sv/resources/templates/releasenotes-md.tpl +++ b/cmd/git-sv/resources/templates/releasenotes-md.tpl @@ -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}} diff --git a/cmd/git-sv/resources/templates/rn-md-section-commits.tpl b/cmd/git-sv/resources/templates/rn-md-section-commits.tpl new file mode 100644 index 0000000..013f90b --- /dev/null +++ b/cmd/git-sv/resources/templates/rn-md-section-commits.tpl @@ -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}} \ No newline at end of file diff --git a/cmd/git-sv/resources/templates/rn-md-section-item.tpl b/cmd/git-sv/resources/templates/rn-md-section-item.tpl deleted file mode 100644 index f0783a3..0000000 --- a/cmd/git-sv/resources/templates/rn-md-section-item.tpl +++ /dev/null @@ -1 +0,0 @@ -- {{if .Message.Scope}}**{{.Message.Scope}}:** {{end}}{{.Message.Description}} ({{.Hash}}){{if .Message.Metadata.issue}} ({{.Message.Metadata.issue}}){{end}} \ No newline at end of file diff --git a/cmd/git-sv/resources/templates/rn-md-section.tpl b/cmd/git-sv/resources/templates/rn-md-section.tpl deleted file mode 100644 index 32c7759..0000000 --- a/cmd/git-sv/resources/templates/rn-md-section.tpl +++ /dev/null @@ -1,7 +0,0 @@ -{{- if .}}{{- if ne .Name ""}} - -### {{.Name}} -{{range $k,$v := .Items}} -{{template "rn-md-section-item.tpl" $v}} -{{- end}} -{{- end}}{{- end}} \ No newline at end of file diff --git a/sv/config.go b/sv/config.go index fda3506..65d1679 100644 --- a/sv/config.go +++ b/sv/config.go @@ -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" ) diff --git a/sv/formatter.go b/sv/formatter.go index 80fa2d1..917d0c5 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -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) +} diff --git a/sv/formatter_test.go b/sv/formatter_test.go index 05acb30..ea29235 100644 --- a/sv/formatter_test.go +++ b/sv/formatter_test.go @@ -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"}}, } } diff --git a/sv/helpers_test.go b/sv/helpers_test.go index 5b17f4d..cc7b66e 100644 --- a/sv/helpers_test.go +++ b/sv/helpers_test.go @@ -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, diff --git a/sv/releasenotes.go b/sv/releasenotes.go index ab0cd7b..316abee 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -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 +} diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index 43f6981..af3029f 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{"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) } From 67042ac1e70dc8b3ec5080582da5770df6d087e1 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 20:47:45 -0300 Subject: [PATCH 09/17] feat: migrate 'release-notes.headers' to 'release-notes.sections' issue: #40 --- cmd/git-sv/config.go | 6 +++--- cmd/git-sv/main.go | 10 ++++++---- sv/config.go | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index 7e84068..4bbd0e2 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -70,7 +70,7 @@ func readConfig(filepath string) (Config, error) { func defaultConfig() Config { skipDetached := false return Config{ - Version: "1.0", + Version: "1.1", Versioning: sv.VersioningConfig{ UpdateMajor: []string{}, UpdateMinor: []string{"feat"}, @@ -136,11 +136,11 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V return nil } -func migrateConfig(cfg Config) Config { +func migrateConfig(cfg Config, filename string) Config { if cfg.ReleaseNotes.Headers == nil { return cfg } - warnf("config 'release-notes.headers' is deprecated, please use 'sections' instead!") + warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename) return Config{ Version: cfg.Version, diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 6ecc1bf..b2a4a02 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -171,15 +171,17 @@ func loadCfg(repoPath string) Config { envCfg := loadEnvConfig() if envCfg.Home != "" { - if homeCfg, err := readConfig(filepath.Join(envCfg.Home, configFilename)); err == nil { - if merr := merge(&cfg, homeCfg); merr != nil { + homeCfgFilepath := filepath.Join(envCfg.Home, configFilename) + if homeCfg, err := readConfig(homeCfgFilepath); err == nil { + if merr := merge(&cfg, migrateConfig(homeCfg, homeCfgFilepath)); merr != nil { log.Fatal("failed to merge user config, error: ", merr) } } } - if repoCfg, err := readConfig(filepath.Join(repoPath, repoConfigFilename)); err == nil { - if merr := merge(&cfg, repoCfg); merr != nil { + repoCfgFilepath := filepath.Join(repoPath, repoConfigFilename) + if repoCfg, err := readConfig(repoCfgFilepath); err == nil { + if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil { log.Fatal("failed to merge repo config, error: ", merr) } if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten diff --git a/sv/config.go b/sv/config.go index 65d1679..af19378 100644 --- a/sv/config.go +++ b/sv/config.go @@ -68,7 +68,7 @@ type TagConfig struct { // ReleaseNotesConfig release notes preferences. type ReleaseNotesConfig struct { - Headers map[string]string `yaml:"headers"` + Headers map[string]string `yaml:"headers,omitempty"` Sections []ReleaseNotesSectionConfig `yaml:"sections"` } @@ -85,7 +85,7 @@ func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSec type ReleaseNotesSectionConfig struct { Name string `yaml:"name"` SectionType string `yaml:"section-type"` - CommitTypes []string `yaml:"commit-types"` + CommitTypes []string `yaml:"commit-types,flow,omitempty"` } const ( From 7d8ed49e17ffc36885c1d5ea1ebdaebc578b560c Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 21:17:37 -0300 Subject: [PATCH 10/17] feat: add HasMultipleTypes to ReleaseNoteCommitsSection issue: #40 --- sv/releasenotes.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sv/releasenotes.go b/sv/releasenotes.go index 316abee..f4a7d85 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -132,3 +132,8 @@ func (ReleaseNoteCommitsSection) SectionType() string { func (s ReleaseNoteCommitsSection) SectionName() string { return s.Name } + +// HasMultipleTypes return true if has more than one commit type. +func (s ReleaseNoteCommitsSection) HasMultipleTypes() bool { + return len(s.Types) > 1 +} From 19bf65fbb3e0ec7e104ed06625b52abffcbc0df3 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 23:37:26 -0300 Subject: [PATCH 11/17] chore: update config version issue: #40 --- .sv4git.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sv4git.yml b/.sv4git.yml index 99fed19..ff2b04d 100644 --- a/.sv4git.yml +++ b/.sv4git.yml @@ -1,4 +1,4 @@ -version: "1.0" +version: "1.1" versioning: update-major: [] From 79df6b030fd2a25bbc0c30ac0976ea33f8e423d2 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 23:38:11 -0300 Subject: [PATCH 12/17] feat: add getsection template function issue: #40 --- sv/formatter.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sv/formatter.go b/sv/formatter.go index 917d0c5..96e8488 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -33,7 +33,8 @@ type OutputFormatterImpl struct { // NewOutputFormatter TemplateProcessor constructor. func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl { templateFNs := map[string]interface{}{ - "timefmt": timeFormat, + "timefmt": timeFormat, + "getSection": getSection, } tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*")) return &OutputFormatterImpl{templates: tpls} @@ -94,3 +95,12 @@ func timeFormat(t time.Time, format string) string { } return t.Format(format) } + +func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection { + for _, section := range sections { + if section.SectionName() == name { + return section + } + } + return nil +} From 7871515ac05412fce777247abc9c73b0d53d0c27 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 23:40:29 -0300 Subject: [PATCH 13/17] refactor: move template funcions to a new go file issue: #40 --- sv/formatter.go | 18 +----------------- sv/formatter_functions.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 sv/formatter_functions.go diff --git a/sv/formatter.go b/sv/formatter.go index 96e8488..61b7ee8 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -34,7 +34,7 @@ type OutputFormatterImpl struct { func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl { templateFNs := map[string]interface{}{ "timefmt": timeFormat, - "getSection": getSection, + "getsection": getSection, } tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*")) return &OutputFormatterImpl{templates: tpls} @@ -88,19 +88,3 @@ 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) -} - -func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection { - for _, section := range sections { - if section.SectionName() == name { - return section - } - } - return nil -} diff --git a/sv/formatter_functions.go b/sv/formatter_functions.go new file mode 100644 index 0000000..607b988 --- /dev/null +++ b/sv/formatter_functions.go @@ -0,0 +1,19 @@ +package sv + +import "time" + +func timeFormat(t time.Time, format string) string { + if t.IsZero() { + return "" + } + return t.Format(format) +} + +func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection { + for _, section := range sections { + if section.SectionName() == name { + return section + } + } + return nil +} From 2c741062f7fc72d2565da7e26cc1b4a40233955f Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 23:43:39 -0300 Subject: [PATCH 14/17] docs: add template and release-notes sections to readme issue: #40 --- README.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cae1622..3b67426 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

sv4git

-

semantic version for git

+

A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs!

Release Go Reference @@ -26,11 +26,17 @@ If you want to install from source using `go install`, just run: ```bash +# keep in mind that with this, it will compile from source and won't show the version on cli -h. go install github.com/bvieira/sv4git/v2/cmd/git-sv@latest + +# if you want to add the version on the binary, run this command instead. +SV4GIT_VERSION=$(go list -f '{{ .Version }}' -m github.com/bvieira/sv4git/v2@latest | sed 's/v//') && go install --ldflags "-X main.Version=$SV4GIT_VERSION" github.com/bvieira/sv4git/v2/cmd/git-sv@v$SV4GIT_VERSION ``` ### Config +#### YAML + There are 3 config levels when using sv4git: [default](#default), [user](#user), [repository](#repository). All of them are merged considering the follow priority: **repository > user > default**. To see the current config, run: @@ -39,9 +45,9 @@ To see the current config, run: git sv cfg show ``` -#### Configuration Types +##### Configuration Types -##### Default +###### Default To check the default configuration, run: @@ -49,7 +55,7 @@ To check the default configuration, run: git sv cfg default ``` -##### User +###### User For user config, it is necessary to define the `SV4GIT_HOME` environment variable, eg.: @@ -64,14 +70,14 @@ And create a `config.yml` file inside it, eg.: └── config.yml ``` -##### Repository +###### Repository Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml). -#### Configuration format +##### Configuration format ```yml -version: "1.0" #config version +version: "1.1" #config version versioning: # versioning bump update-major: [] # Commit types used to bump major. @@ -85,13 +91,23 @@ tag: pattern: '%d.%d.%d' # Pattern used to create git tag. release-notes: - # Headers names for release notes markdown. To disable a section just remove the header line. - # It's possible to add other commit types, the release note will be created respecting the following order: - # feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change + # Deprecated!!! please use 'sections' instead! Headers names for release notes markdown. To disable a section + # just remove the header line. It's possible to add other commit types, the release note will be created + # respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change. headers: breaking-change: Breaking Changes feat: Features fix: Bug Fixes + + sections: # Array with each section of release note. Check template section for more information. + - name: Features # Name used on section name. + section-type: commits # Type of the section, supported types: commits, breaking-changes. + commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section. + - name: Bug Fixes + section-type: commits + commit-types: [fix] + - name: Breaking Changes + section-type: breaking-changes branches: # Git branches config. prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group. @@ -116,6 +132,95 @@ commit-message: regex: '[A-Z]+-[0-9]+' # Regex for issue id. ``` +#### Templates + +**sv4git** uses go templates to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). On v2.7.0+, its possible to overwrite the default configuration by adding `.sv4git/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`. + +```bash +.sv4git +└── templates + ├── changelog-md.tpl + └── releasenotes-md.tpl +``` + +Everything inside `.sv4git/templates` will be loaded, so it's possible to add more files to be used as needed. + +##### Variables + +To execute the template the `releasenotes-md.tpl` will receive a single ReleaseNote and `changelog-md.tpl` will receive a list of **ReleaseNote** as variables. + +Each **ReleaseNoteSection** will be configured according with `release-notes.section` from config file. The order for each section will be maintained and the SectionType is defined according with `section-type` attribute as described on the table below. + +| section-type | ReleaseNoteSection | +| -- | -- | +| commits | ReleaseNoteCommitsSection | +| breaking-changes | ReleaseNoteBreakingChangeSection | + +> warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables. + +Check below the variables available: + +```go +ReleaseNote + Release string // If has a version, it will be used, if not use tag instead. + Tag string // Current tag, if available. + Version *Version // Version from tag or next version according with semver. + Date time.Time + Sections []ReleaseNoteSection // ReleaseNoteCommitsSection or ReleaseNoteBreakingChangeSection + AuthorNames []string // Author names recovered from commit message (user.name from git) + +Version + Major int + Minor int + Patch int + Prerelease string + Metadata string + Original string + +ReleaseNoteCommitsSection (SectionType == commits) + SectionType string + SectionName string + Types []string + Items []GitCommitLog + HasMultipleTypes bool + +ReleaseNoteBreakingChangeSection (SectionType == breaking-changes) + SectionType string + SectionName string + Messages []string + +GitCommitLog + Date string + Timestamp int + AuthorName string + Hash string + Message CommitMessage + +CommitMessage + Type string + Scope string + Description string + Body string + IsBreakingChange bool + Metadata map[string]string +``` + +##### Functions + +Beside the [go template functions](https://pkg.go.dev/text/template#hdr-Functions), the folowing functions are availiable to use in the templates. Check [formatter_functions.go](sv/formatter_functions.go) to see the function implementation. + +###### timefmt + +**Usage:** timefmt time "2006-01-02" + +Receive a time.Time and a layout string and returns a textual representation of the time according with the layout provided. Check for more information. + +###### getsection + +**Usage:** getsection sections "Features" + +Receive a list of ReleaseNoteSection and a Section name and returns a section with the provided name. If no section is found, it will return `nil`. + ### Running Run `git-sv` to get the list of available parameters: From 2df7762e8f2dcad4a2b4f31c938d2cd2e669d73a Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 23:52:46 -0300 Subject: [PATCH 15/17] docs: minor fixes on readme issue: #40 --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3b67426..6b7c7e1 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,9 @@ tag: pattern: '%d.%d.%d' # Pattern used to create git tag. release-notes: - # Deprecated!!! please use 'sections' instead! Headers names for release notes markdown. To disable a section - # just remove the header line. It's possible to add other commit types, the release note will be created + # Deprecated!!! please use 'sections' instead! + # Headers names for release notes markdown. To disable a section just remove the header + # line. It's possible to add other commit types, the release note will be created # respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change. headers: breaking-change: Breaking Changes @@ -100,7 +101,7 @@ release-notes: fix: Bug Fixes sections: # Array with each section of release note. Check template section for more information. - - name: Features # Name used on section name. + - name: Features # Name used on section. section-type: commits # Type of the section, supported types: commits, breaking-changes. commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section. - name: Bug Fixes @@ -134,7 +135,7 @@ commit-message: #### Templates -**sv4git** uses go templates to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). On v2.7.0+, its possible to overwrite the default configuration by adding `.sv4git/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`. +**sv4git** uses *go templates* to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). On v2.7.0+, its possible to overwrite the default configuration by adding `.sv4git/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`. ```bash .sv4git @@ -147,22 +148,22 @@ Everything inside `.sv4git/templates` will be loaded, so it's possible to add mo ##### Variables -To execute the template the `releasenotes-md.tpl` will receive a single ReleaseNote and `changelog-md.tpl` will receive a list of **ReleaseNote** as variables. +To execute the template the `releasenotes-md.tpl` will receive a single **ReleaseNote** and `changelog-md.tpl` will receive a list of **ReleaseNote** as variables. -Each **ReleaseNoteSection** will be configured according with `release-notes.section` from config file. The order for each section will be maintained and the SectionType is defined according with `section-type` attribute as described on the table below. +Each **ReleaseNoteSection** will be configured according with `release-notes.section` from config file. The order for each section will be maintained and the **SectionType** is defined according with `section-type` attribute as described on the table below. | section-type | ReleaseNoteSection | | -- | -- | | commits | ReleaseNoteCommitsSection | | breaking-changes | ReleaseNoteBreakingChangeSection | -> warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables. +> :warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables. Check below the variables available: ```go ReleaseNote - Release string // If has a version, it will be used, if not use tag instead. + Release string // 'v' followed by version if present, if not tag will be used instead. Tag string // Current tag, if available. Version *Version // Version from tag or next version according with semver. Date time.Time From 91aaf8410810c368cb496c8df0ec584e304ce335 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 28 Feb 2022 23:53:56 -0300 Subject: [PATCH 16/17] docs: minor fixes on readme issue: #40 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6b7c7e1..b2c6d3e 100644 --- a/README.md +++ b/README.md @@ -178,14 +178,14 @@ Version Metadata string Original string -ReleaseNoteCommitsSection (SectionType == commits) +ReleaseNoteCommitsSection // SectionType == commits SectionType string SectionName string Types []string Items []GitCommitLog HasMultipleTypes bool -ReleaseNoteBreakingChangeSection (SectionType == breaking-changes) +ReleaseNoteBreakingChangeSection // SectionType == breaking-changes SectionType string SectionName string Messages []string @@ -208,7 +208,7 @@ CommitMessage ##### Functions -Beside the [go template functions](https://pkg.go.dev/text/template#hdr-Functions), the folowing functions are availiable to use in the templates. Check [formatter_functions.go](sv/formatter_functions.go) to see the function implementation. +Beside the [go template functions](https://pkg.go.dev/text/template#hdr-Functions), the folowing functions are availiable to use in the templates. Check [formatter_functions.go](sv/formatter_functions.go) to see the functions implementation. ###### timefmt From 893f893d6c59146c229857d5305db54086106c53 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Tue, 1 Mar 2022 00:03:15 -0300 Subject: [PATCH 17/17] test: add unit test for template functions issue: #40 --- sv/formatter_functions_test.go | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 sv/formatter_functions_test.go diff --git a/sv/formatter_functions_test.go b/sv/formatter_functions_test.go new file mode 100644 index 0000000..83a2d92 --- /dev/null +++ b/sv/formatter_functions_test.go @@ -0,0 +1,45 @@ +package sv + +import ( + "reflect" + "testing" + "time" +) + +func Test_timeFormat(t *testing.T) { + tests := []struct { + name string + time time.Time + format string + want string + }{ + {"valid time", time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), "2006-01-02", "2022-01-01"}, + {"empty time", time.Time{}, "2006-01-02", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := timeFormat(tt.time, tt.format); got != tt.want { + t.Errorf("timeFormat() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getSection(t *testing.T) { + tests := []struct { + name string + sections []ReleaseNoteSection + sectionName string + want ReleaseNoteSection + }{ + {"existing section", []ReleaseNoteSection{ReleaseNoteCommitsSection{Name: "section 0"}, ReleaseNoteCommitsSection{Name: "section 1"}, ReleaseNoteCommitsSection{Name: "section 2"}}, "section 1", ReleaseNoteCommitsSection{Name: "section 1"}}, + {"nonexisting section", []ReleaseNoteSection{ReleaseNoteCommitsSection{Name: "section 0"}, ReleaseNoteCommitsSection{Name: "section 1"}, ReleaseNoteCommitsSection{Name: "section 2"}}, "section 10", nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getSection(tt.sections, tt.sectionName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getSection() = %v, want %v", got, tt.want) + } + }) + } +}