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/cmd/git-sv/resources/templates/changelog-md.tpl b/cmd/git-sv/resources/templates/changelog-md.tpl new file mode 100644 index 0000000..d478272 --- /dev/null +++ b/cmd/git-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/cmd/git-sv/resources/templates/releasenotes-md.tpl b/cmd/git-sv/resources/templates/releasenotes-md.tpl new file mode 100644 index 0000000..440cb6b --- /dev/null +++ b/cmd/git-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/cmd/git-sv/resources/templates/rn-md-section-breaking-changes.tpl b/cmd/git-sv/resources/templates/rn-md-section-breaking-changes.tpl new file mode 100644 index 0000000..862b1dc --- /dev/null +++ b/cmd/git-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/cmd/git-sv/resources/templates/rn-md-section-item.tpl b/cmd/git-sv/resources/templates/rn-md-section-item.tpl new file mode 100644 index 0000000..f0783a3 --- /dev/null +++ b/cmd/git-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/cmd/git-sv/resources/templates/rn-md-section.tpl b/cmd/git-sv/resources/templates/rn-md-section.tpl new file mode 100644 index 0000000..32c7759 --- /dev/null +++ b/cmd/git-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 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 9d93367..f47d345 100644 --- a/sv/formatter.go +++ b/sv/formatter.go @@ -2,6 +2,7 @@ package sv import ( "bytes" + "io/fs" "text/template" ) @@ -13,42 +14,6 @@ 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}} -` -) - // OutputFormatter output formatter interface. type OutputFormatter interface { FormatReleaseNote(releasenote ReleaseNote) (string, error) @@ -57,24 +22,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} +func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl { + tpls := template.Must(template.New("templates").ParseFS(templatesFS, "*")) + 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 +48,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..e186548 100644 --- a/sv/formatter_test.go +++ b/sv/formatter_test.go @@ -1,12 +1,17 @@ package sv import ( + "bytes" + "os" "testing" + "text/template" "time" "github.com/Masterminds/semver/v3" ) +var templatesFS = os.DirFS("../cmd/git-sv/resources/templates") + var dateChangelog = `## v1.0.0 (2020-05-01) ` @@ -55,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) } @@ -85,3 +90,50 @@ 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, + 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 + +}