mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-21 22:10:39 +00:00
Merge pull request #43 from bvieira/#40
Feature: support templates for release-notes and changelog
This commit is contained in:
commit
cf43b2af4e
@ -1,4 +1,4 @@
|
|||||||
version: "1.0"
|
version: "1.1"
|
||||||
|
|
||||||
versioning:
|
versioning:
|
||||||
update-major: []
|
update-major: []
|
||||||
|
126
README.md
126
README.md
@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<h1 align="center">sv4git</h1>
|
<h1 align="center">sv4git</h1>
|
||||||
<p align="center">semantic version for git</p>
|
<p align="center">A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs!</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/bvieira/sv4git.svg?style=for-the-badge"></a>
|
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/bvieira/sv4git.svg?style=for-the-badge"></a>
|
||||||
<a href="https://pkg.go.dev/github.com/bvieira/sv4git/v2"><img alt="Go Reference" src="https://img.shields.io/badge/-Reference-blue?style=for-the-badge&logo=go&labelColor=gray"></a>
|
<a href="https://pkg.go.dev/github.com/bvieira/sv4git/v2"><img alt="Go Reference" src="https://img.shields.io/badge/-Reference-blue?style=for-the-badge&logo=go&labelColor=gray"></a>
|
||||||
@ -26,11 +26,17 @@
|
|||||||
If you want to install from source using `go install`, just run:
|
If you want to install from source using `go install`, just run:
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
### 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**.
|
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:
|
To see the current config, run:
|
||||||
@ -39,9 +45,9 @@ To see the current config, run:
|
|||||||
git sv cfg show
|
git sv cfg show
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Configuration Types
|
##### Configuration Types
|
||||||
|
|
||||||
##### Default
|
###### Default
|
||||||
|
|
||||||
To check the default configuration, run:
|
To check the default configuration, run:
|
||||||
|
|
||||||
@ -49,7 +55,7 @@ To check the default configuration, run:
|
|||||||
git sv cfg default
|
git sv cfg default
|
||||||
```
|
```
|
||||||
|
|
||||||
##### User
|
###### User
|
||||||
|
|
||||||
For user config, it is necessary to define the `SV4GIT_HOME` environment variable, eg.:
|
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
|
└── config.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Repository
|
###### Repository
|
||||||
|
|
||||||
Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml).
|
Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml).
|
||||||
|
|
||||||
#### Configuration format
|
##### Configuration format
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
version: "1.0" #config version
|
version: "1.1" #config version
|
||||||
|
|
||||||
versioning: # versioning bump
|
versioning: # versioning bump
|
||||||
update-major: [] # Commit types used to bump major.
|
update-major: [] # Commit types used to bump major.
|
||||||
@ -85,14 +91,25 @@ tag:
|
|||||||
pattern: '%d.%d.%d' # Pattern used to create git tag.
|
pattern: '%d.%d.%d' # Pattern used to create git tag.
|
||||||
|
|
||||||
release-notes:
|
release-notes:
|
||||||
# Headers names for release notes markdown. To disable a section just remove the header line.
|
# Deprecated!!! please use 'sections' instead!
|
||||||
# It's possible to add other commit types, the release note will be created respecting the following order:
|
# Headers names for release notes markdown. To disable a section just remove the header
|
||||||
# feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change
|
# 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:
|
headers:
|
||||||
breaking-change: Breaking Changes
|
breaking-change: Breaking Changes
|
||||||
feat: Features
|
feat: Features
|
||||||
fix: Bug Fixes
|
fix: Bug Fixes
|
||||||
|
|
||||||
|
sections: # Array with each section of release note. Check template section for more information.
|
||||||
|
- 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
|
||||||
|
section-type: commits
|
||||||
|
commit-types: [fix]
|
||||||
|
- name: Breaking Changes
|
||||||
|
section-type: breaking-changes
|
||||||
|
|
||||||
branches: # Git branches config.
|
branches: # Git branches config.
|
||||||
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group.
|
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group.
|
||||||
suffix: (-.*)? # Suffix used on branch name, it should be a regex group.
|
suffix: (-.*)? # Suffix used on branch name, it should be a regex group.
|
||||||
@ -116,6 +133,95 @@ commit-message:
|
|||||||
regex: '[A-Z]+-[0-9]+' # Regex for issue id.
|
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 // '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
|
||||||
|
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 functions 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 <https://pkg.go.dev/time#Time.Format> 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
|
### Running
|
||||||
|
|
||||||
Run `git-sv` to get the list of available parameters:
|
Run `git-sv` to get the list of available parameters:
|
||||||
|
@ -70,15 +70,21 @@ func readConfig(filepath string) (Config, error) {
|
|||||||
func defaultConfig() Config {
|
func defaultConfig() Config {
|
||||||
skipDetached := false
|
skipDetached := false
|
||||||
return Config{
|
return Config{
|
||||||
Version: "1.0",
|
Version: "1.1",
|
||||||
Versioning: sv.VersioningConfig{
|
Versioning: sv.VersioningConfig{
|
||||||
UpdateMajor: []string{},
|
UpdateMajor: []string{},
|
||||||
UpdateMinor: []string{"feat"},
|
UpdateMinor: []string{"feat"},
|
||||||
UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
|
UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
|
||||||
IgnoreUnknown: false,
|
IgnoreUnknown: false,
|
||||||
},
|
},
|
||||||
Tag: sv.TagConfig{Pattern: "%d.%d.%d"},
|
Tag: sv.TagConfig{Pattern: "%d.%d.%d"},
|
||||||
ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"fix": "Bug Fixes", "feat": "Features", "breaking-change": "Breaking Changes"}},
|
ReleaseNotes: sv.ReleaseNotesConfig{
|
||||||
|
Sections: []sv.ReleaseNotesSectionConfig{
|
||||||
|
{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{
|
Branches: sv.BranchesConfig{
|
||||||
Prefix: "([a-z]+\\/)?",
|
Prefix: "([a-z]+\\/)?",
|
||||||
Suffix: "(-.*)?",
|
Suffix: "(-.*)?",
|
||||||
@ -129,3 +135,35 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateConfig(cfg Config, filename string) Config {
|
||||||
|
if cfg.ReleaseNotes.Headers == nil {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
|
||||||
|
|
||||||
|
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.ReleaseNotesSectionTypeBreakingChanges})
|
||||||
|
}
|
||||||
|
return sections
|
||||||
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
func warnf(format string, values ...interface{}) {
|
func warnf(format string, values ...interface{}) {
|
||||||
fmt.Printf("WARN: "+format+"\n", values...)
|
fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", values...)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -15,17 +17,36 @@ var Version = "source"
|
|||||||
const (
|
const (
|
||||||
configFilename = "config.yml"
|
configFilename = "config.yml"
|
||||||
repoConfigFilename = ".sv4git.yml"
|
repoConfigFilename = ".sv4git.yml"
|
||||||
|
configDir = ".sv4git"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed resources/templates/*.tpl
|
||||||
|
defaultTemplatesFS embed.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
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() {
|
func main() {
|
||||||
log.SetFlags(0)
|
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)
|
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
|
||||||
git := sv.NewGit(messageProcessor, cfg.Tag)
|
git := sv.NewGit(messageProcessor, cfg.Tag)
|
||||||
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
|
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
|
||||||
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
|
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
|
||||||
outputFormatter := sv.NewOutputFormatter()
|
outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(repoPath, configDir, "templates")))
|
||||||
|
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "sv"
|
app.Name = "sv"
|
||||||
@ -145,26 +166,22 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCfg() Config {
|
func loadCfg(repoPath string) Config {
|
||||||
envCfg := loadEnvConfig()
|
|
||||||
|
|
||||||
cfg := defaultConfig()
|
cfg := defaultConfig()
|
||||||
|
|
||||||
|
envCfg := loadEnvConfig()
|
||||||
if envCfg.Home != "" {
|
if envCfg.Home != "" {
|
||||||
if homeCfg, err := readConfig(filepath.Join(envCfg.Home, configFilename)); err == nil {
|
homeCfgFilepath := filepath.Join(envCfg.Home, configFilename)
|
||||||
if merr := merge(&cfg, homeCfg); merr != nil {
|
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)
|
log.Fatal("failed to merge user config, error: ", merr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repoPath, rerr := getRepoPath()
|
repoCfgFilepath := filepath.Join(repoPath, repoConfigFilename)
|
||||||
if rerr != nil {
|
if repoCfg, err := readConfig(repoCfgFilepath); err == nil {
|
||||||
log.Fatal("failed to get repository path, error: ", rerr)
|
if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil {
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
log.Fatal("failed to merge repo config, error: ", merr)
|
||||||
}
|
}
|
||||||
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
|
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
|
||||||
|
6
cmd/git-sv/resources/templates/changelog-md.tpl
Normal file
6
cmd/git-sv/resources/templates/changelog-md.tpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Changelog
|
||||||
|
{{- range .}}
|
||||||
|
|
||||||
|
{{template "releasenotes-md.tpl" .}}
|
||||||
|
---
|
||||||
|
{{- end}}
|
8
cmd/git-sv/resources/templates/releasenotes-md.tpl
Normal file
8
cmd/git-sv/resources/templates/releasenotes-md.tpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
## {{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 }}
|
||||||
|
{{- 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}}
|
@ -0,0 +1,7 @@
|
|||||||
|
{{- if ne .Name ""}}
|
||||||
|
|
||||||
|
### {{.Name}}
|
||||||
|
{{range $k,$v := .Messages}}
|
||||||
|
- {{$v}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
7
cmd/git-sv/resources/templates/rn-md-section-commits.tpl
Normal file
7
cmd/git-sv/resources/templates/rn-md-section-commits.tpl
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{{- if .}}{{- if ne .SectionName ""}}
|
||||||
|
|
||||||
|
### {{.SectionName}}
|
||||||
|
{{range $k,$v := .Items}}
|
||||||
|
- {{if $v.Message.Scope}}**{{$v.Message.Scope}}:** {{end}}{{$v.Message.Description}} ({{$v.Hash}}){{if $v.Message.Metadata.issue}} ({{$v.Message.Metadata.issue}}){{end}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}{{- end}}
|
24
cmd/git-sv/resources_test.go
Normal file
24
cmd/git-sv/resources_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
26
sv/config.go
26
sv/config.go
@ -68,5 +68,29 @@ type TagConfig struct {
|
|||||||
|
|
||||||
// ReleaseNotesConfig release notes preferences.
|
// ReleaseNotesConfig release notes preferences.
|
||||||
type ReleaseNotesConfig struct {
|
type ReleaseNotesConfig struct {
|
||||||
Headers map[string]string `yaml:"headers"`
|
Headers map[string]string `yaml:"headers,omitempty"`
|
||||||
|
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,flow,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value.
|
||||||
|
ReleaseNotesSectionTypeCommits = "commits"
|
||||||
|
// ReleaseNotesSectionTypeBreakingChanges ReleaseNotesSectionConfig.SectionType value.
|
||||||
|
ReleaseNotesSectionTypeBreakingChanges = "breaking-changes"
|
||||||
|
)
|
||||||
|
104
sv/formatter.go
104
sv/formatter.go
@ -2,53 +2,23 @@ package sv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io/fs"
|
||||||
|
"sort"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type releaseNoteTemplateVariables struct {
|
type releaseNoteTemplateVariables struct {
|
||||||
Release string
|
Release string
|
||||||
Date string
|
Tag string
|
||||||
Sections map[string]ReleaseNoteSection
|
Version *semver.Version
|
||||||
Order []string
|
Date time.Time
|
||||||
BreakingChanges BreakingChangeSection
|
Sections []ReleaseNoteSection
|
||||||
|
AuthorNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// OutputFormatter output formatter interface.
|
||||||
type OutputFormatter interface {
|
type OutputFormatter interface {
|
||||||
FormatReleaseNote(releasenote ReleaseNote) (string, error)
|
FormatReleaseNote(releasenote ReleaseNote) (string, error)
|
||||||
@ -57,24 +27,23 @@ type OutputFormatter interface {
|
|||||||
|
|
||||||
// OutputFormatterImpl formater for release note and changelog.
|
// OutputFormatterImpl formater for release note and changelog.
|
||||||
type OutputFormatterImpl struct {
|
type OutputFormatterImpl struct {
|
||||||
releasenoteTemplate *template.Template
|
templates *template.Template
|
||||||
changelogTemplate *template.Template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOutputFormatter TemplateProcessor constructor.
|
// NewOutputFormatter TemplateProcessor constructor.
|
||||||
func NewOutputFormatter() *OutputFormatterImpl {
|
func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl {
|
||||||
cgl := template.Must(template.New("cglTemplate").Parse(cglTemplate))
|
templateFNs := map[string]interface{}{
|
||||||
rn := template.Must(cgl.New("rnTemplate").Parse(rnTemplate))
|
"timefmt": timeFormat,
|
||||||
template.Must(rn.New("rnSectionItem").Parse(rnSectionItem))
|
"getsection": getSection,
|
||||||
template.Must(rn.New("rnSection").Parse(rnSection))
|
}
|
||||||
template.Must(rn.New("rnSectionBreakingChanges").Parse(rnSectionBreakingChanges))
|
tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*"))
|
||||||
return &OutputFormatterImpl{releasenoteTemplate: rn, changelogTemplate: cgl}
|
return &OutputFormatterImpl{templates: tpls}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatReleaseNote format a release note.
|
// FormatReleaseNote format a release note.
|
||||||
func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string, error) {
|
func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string, error) {
|
||||||
var b bytes.Buffer
|
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 "", err
|
||||||
}
|
}
|
||||||
return b.String(), nil
|
return b.String(), nil
|
||||||
@ -88,29 +57,34 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string
|
|||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
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 "", err
|
||||||
}
|
}
|
||||||
return b.String(), nil
|
return b.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables {
|
func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables {
|
||||||
date := ""
|
release := releasenote.Tag
|
||||||
if !releasenote.Date.IsZero() {
|
|
||||||
date = releasenote.Date.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
|
|
||||||
release := ""
|
|
||||||
if releasenote.Version != nil {
|
if releasenote.Version != nil {
|
||||||
release = "v" + releasenote.Version.String()
|
release = "v" + releasenote.Version.String()
|
||||||
} else if releasenote.Tag != "" {
|
|
||||||
release = releasenote.Tag
|
|
||||||
}
|
}
|
||||||
return releaseNoteTemplateVariables{
|
return releaseNoteTemplateVariables{
|
||||||
Release: release,
|
Release: release,
|
||||||
Date: date,
|
Tag: releasenote.Tag,
|
||||||
Sections: releasenote.Sections,
|
Version: releasenote.Version,
|
||||||
Order: []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"},
|
Date: releasenote.Date,
|
||||||
BreakingChanges: releasenote.BreakingChanges,
|
Sections: releasenote.Sections,
|
||||||
|
AuthorNames: toSortedArray(releasenote.AuthorsNames),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
19
sv/formatter_functions.go
Normal file
19
sv/formatter_functions.go
Normal file
@ -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
|
||||||
|
}
|
45
sv/formatter_functions_test.go
Normal file
45
sv/formatter_functions_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,16 @@
|
|||||||
package sv
|
package sv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var templatesFS = os.DirFS("../cmd/git-sv/resources/templates")
|
||||||
|
|
||||||
var dateChangelog = `## v1.0.0 (2020-05-01)
|
var dateChangelog = `## v1.0.0 (2020-05-01)
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -55,7 +59,7 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
if got != tt.want {
|
||||||
t.Errorf("OutputFormatterImpl.FormatReleaseNote() = %v, want %v", got, tt.want)
|
t.Errorf("OutputFormatterImpl.FormatReleaseNote() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
@ -78,10 +82,57 @@ func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
|
|||||||
|
|
||||||
func fullReleaseNote(tag string, date time.Time) ReleaseNote {
|
func fullReleaseNote(tag string, date time.Time) ReleaseNote {
|
||||||
v, _ := semver.NewVersion(tag)
|
v, _ := semver.NewVersion(tag)
|
||||||
sections := map[string]ReleaseNoteSection{
|
sections := []ReleaseNoteSection{
|
||||||
"build": newReleaseNoteSection("Build", []GitCommitLog{commitlog("build", map[string]string{})}),
|
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
|
||||||
"feat": newReleaseNoteSection("Features", []GitCommitLog{commitlog("feat", map[string]string{})}),
|
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
|
||||||
"fix": newReleaseNoteSection("Bug Fixes", []GitCommitLog{commitlog("fix", map[string]string{})}),
|
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"})
|
return releaseNote(v, tag, date, sections, map[string]struct{}{"a": {}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_checkTemplatesExecution(t *testing.T) {
|
||||||
|
tpls := NewOutputFormatter(templatesFS).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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseNotesVariables(release string) releaseNoteTemplateVariables {
|
||||||
|
return releaseNoteTemplateVariables{
|
||||||
|
Release: release,
|
||||||
|
Date: time.Date(2006, 1, 02, 0, 0, 0, 0, time.UTC),
|
||||||
|
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"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func changelogVariables(releases ...string) []releaseNoteTemplateVariables {
|
||||||
|
var variables []releaseNoteTemplateVariables
|
||||||
|
for _, r := range releases {
|
||||||
|
variables = append(variables, releaseNotesVariables(r))
|
||||||
|
}
|
||||||
|
return variables
|
||||||
|
|
||||||
}
|
}
|
||||||
|
20
sv/git.go
20
sv/git.go
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -31,9 +32,11 @@ type Git interface {
|
|||||||
|
|
||||||
// GitCommitLog description of a single commit log.
|
// GitCommitLog description of a single commit log.
|
||||||
type GitCommitLog struct {
|
type GitCommitLog struct {
|
||||||
Date string `json:"date,omitempty"`
|
Date string `json:"date,omitempty"`
|
||||||
Hash string `json:"hash,omitempty"`
|
Timestamp int `json:"timestamp,omitempty"`
|
||||||
Message CommitMessage `json:"message,omitempty"`
|
AuthorName string `json:"authorName,omitempty"`
|
||||||
|
Hash string `json:"hash,omitempty"`
|
||||||
|
Message CommitMessage `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitTag git tag info.
|
// GitTag git tag info.
|
||||||
@ -90,7 +93,7 @@ func (GitImpl) LastTag() string {
|
|||||||
|
|
||||||
// Log return git log.
|
// Log return git log.
|
||||||
func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
|
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}
|
params := []string{"log", "--date=short", format}
|
||||||
|
|
||||||
if lr.start != "" || lr.end != "" {
|
if lr.start != "" || lr.end != "" {
|
||||||
@ -200,10 +203,13 @@ func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLo
|
|||||||
func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog {
|
func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog {
|
||||||
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
|
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
|
||||||
|
|
||||||
|
timestamp, _ := strconv.Atoi(content[1])
|
||||||
return GitCommitLog{
|
return GitCommitLog{
|
||||||
Date: content[0],
|
Date: content[0],
|
||||||
Hash: content[1],
|
Timestamp: timestamp,
|
||||||
Message: messageProcessor.Parse(content[2], content[3]),
|
AuthorName: content[2],
|
||||||
|
Hash: content[3],
|
||||||
|
Message: messageProcessor.Parse(content[4], content[5]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ func version(v string) *semver.Version {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func commitlog(ctype string, metadata map[string]string) GitCommitLog {
|
func commitlog(ctype string, metadata map[string]string, author string) GitCommitLog {
|
||||||
breaking := false
|
breaking := false
|
||||||
if _, found := metadata[breakingChangeMetadataKey]; found {
|
if _, found := metadata[breakingChangeMetadataKey]; found {
|
||||||
breaking = true
|
breaking = true
|
||||||
@ -23,26 +23,24 @@ func commitlog(ctype string, metadata map[string]string) GitCommitLog {
|
|||||||
IsBreakingChange: breaking,
|
IsBreakingChange: breaking,
|
||||||
Metadata: metadata,
|
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 []ReleaseNoteSection, authorsNames map[string]struct{}) ReleaseNote {
|
||||||
var bchanges BreakingChangeSection
|
|
||||||
if len(breakingChanges) > 0 {
|
|
||||||
bchanges = BreakingChangeSection{Name: "Breaking Changes", Messages: breakingChanges}
|
|
||||||
}
|
|
||||||
return ReleaseNote{
|
return ReleaseNote{
|
||||||
Version: version,
|
Version: version,
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
Date: date.Truncate(time.Minute),
|
Date: date.Truncate(time.Minute),
|
||||||
Sections: sections,
|
Sections: sections,
|
||||||
BreakingChanges: bchanges,
|
AuthorsNames: authorsNames,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newReleaseNoteSection(name string, items []GitCommitLog) ReleaseNoteSection {
|
func newReleaseNoteCommitsSection(name string, types []string, items []GitCommitLog) ReleaseNoteCommitsSection {
|
||||||
return ReleaseNoteSection{
|
return ReleaseNoteCommitsSection{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
Types: types,
|
||||||
Items: items,
|
Items: items,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,16 +23,20 @@ func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
|
|||||||
|
|
||||||
// Create create a release note based on commits.
|
// Create create a release note based on commits.
|
||||||
func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote {
|
func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote {
|
||||||
sections := make(map[string]ReleaseNoteSection)
|
mapping := commitSectionMapping(p.cfg.Sections)
|
||||||
|
|
||||||
|
sections := make(map[string]ReleaseNoteCommitsSection)
|
||||||
|
authors := make(map[string]struct{})
|
||||||
var breakingChanges []string
|
var breakingChanges []string
|
||||||
for _, commit := range commits {
|
for _, commit := range commits {
|
||||||
if name, exists := p.cfg.Headers[commit.Message.Type]; exists {
|
authors[commit.AuthorName] = struct{}{}
|
||||||
section, sexists := sections[commit.Message.Type]
|
if sectionCfg, exists := mapping[commit.Message.Type]; exists {
|
||||||
|
section, sexists := sections[sectionCfg.Name]
|
||||||
if !sexists {
|
if !sexists {
|
||||||
section = ReleaseNoteSection{Name: name}
|
section = ReleaseNoteCommitsSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes}
|
||||||
}
|
}
|
||||||
section.Items = append(section.Items, commit)
|
section.Items = append(section.Items, commit)
|
||||||
sections[commit.Message.Type] = section
|
sections[sectionCfg.Name] = section
|
||||||
}
|
}
|
||||||
if commit.Message.BreakingMessage() != "" {
|
if commit.Message.BreakingMessage() != "" {
|
||||||
// TODO: if no message found, should use description instead?
|
// TODO: if no message found, should use description instead?
|
||||||
@ -40,30 +44,96 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var breakingChangeSection BreakingChangeSection
|
var breakingChangeSection ReleaseNoteBreakingChangeSection
|
||||||
if name, exists := p.cfg.Headers[breakingChangeMetadataKey]; exists && len(breakingChanges) > 0 {
|
if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChanges); bcCfg != nil && len(breakingChanges) > 0 {
|
||||||
breakingChangeSection = BreakingChangeSection{Name: name, Messages: breakingChanges}
|
breakingChangeSection = ReleaseNoteBreakingChangeSection{Name: bcCfg.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: 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 {
|
||||||
|
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.
|
// ReleaseNote release note.
|
||||||
type ReleaseNote struct {
|
type ReleaseNote struct {
|
||||||
Version *semver.Version
|
Version *semver.Version
|
||||||
Tag string
|
Tag string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Sections map[string]ReleaseNoteSection
|
Sections []ReleaseNoteSection
|
||||||
BreakingChanges BreakingChangeSection
|
AuthorsNames map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreakingChangeSection breaking change section.
|
// ReleaseNoteSection section in release notes.
|
||||||
type BreakingChangeSection struct {
|
type ReleaseNoteSection interface {
|
||||||
|
SectionType() string
|
||||||
|
SectionName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseNoteBreakingChangeSection breaking change section.
|
||||||
|
type ReleaseNoteBreakingChangeSection struct {
|
||||||
Name string
|
Name string
|
||||||
Messages []string
|
Messages []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReleaseNoteSection release note section.
|
// SectionType section type.
|
||||||
type ReleaseNoteSection struct {
|
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
|
Name string
|
||||||
|
Types []string
|
||||||
Items []GitCommitLog
|
Items []GitCommitLog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SectionType section type.
|
||||||
|
func (ReleaseNoteCommitsSection) SectionType() string {
|
||||||
|
return ReleaseNotesSectionTypeCommits
|
||||||
|
}
|
||||||
|
|
||||||
|
// SectionName section name.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -24,29 +24,37 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
|
|||||||
version: semver.MustParse("1.0.0"),
|
version: semver.MustParse("1.0.0"),
|
||||||
tag: "v1.0.0",
|
tag: "v1.0.0",
|
||||||
date: date,
|
date: date,
|
||||||
commits: []GitCommitLog{commitlog("t1", map[string]string{})},
|
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", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil),
|
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",
|
name: "unmapped tag",
|
||||||
version: semver.MustParse("1.0.0"),
|
version: semver.MustParse("1.0.0"),
|
||||||
tag: "v1.0.0",
|
tag: "v1.0.0",
|
||||||
date: date,
|
date: date,
|
||||||
commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{})},
|
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", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil),
|
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",
|
name: "breaking changes tag",
|
||||||
version: semver.MustParse("1.0.0"),
|
version: semver.MustParse("1.0.0"),
|
||||||
tag: "v1.0.0",
|
tag: "v1.0.0",
|
||||||
date: date,
|
date: date,
|
||||||
commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breaking-change": "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", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}),
|
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",
|
||||||
|
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, []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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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-changes"}}})
|
||||||
if got := p.Create(tt.version, tt.tag, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
|
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)
|
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
|
@ -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", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0"), false},
|
||||||
{"no update without version", true, nil, []GitCommitLog{}, nil, 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 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{})}, 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{})}, version("0.0.1"), true},
|
{"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{})}, 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{})}, nil, 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{}), commitlog("minor", map[string]string{})}, version("0.1.0"), 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{}), commitlog("major", map[string]string{})}, version("1.0.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{}), commitlog("patch", map[string]string{"breaking-change": "break"})}, 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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user