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

Merge pull request #10 from bvieira/2.x

Feature: yaml config
This commit is contained in:
Beatriz Vieira 2021-02-17 21:30:13 -03:00 committed by GitHub
commit a07a355164
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 861 additions and 302 deletions

21
.sv4git.yml Normal file
View File

@ -0,0 +1,21 @@
version: "1.0"
versioning:
update-major: []
update-minor:
- feat
update-patch:
- build
- ci
- chore
- fix
- perf
- refactor
- test
commit-message:
footer:
issue:
key: issue
issue:
regex: '#[0-9]+'

165
README.md
View File

@ -1,32 +1,130 @@
# sv4git <p align="center">
<h1 align="center">sv4git</h1>
Semantic version for git <p align="center">semantic version for git</p>
<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/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/bvieira/sv4git?style=for-the-badge"></a>
<a href="/LICENSE"><img alt="Software License" src="https://img.shields.io/badge/license-MIT-informational.svg?style=for-the-badge"></a>
<a href="https://github.com/bvieira/sv4git/actions?workflow=ci"><img alt="GitHub Actions" src="https://img.shields.io/github/workflow/status/bvieira/sv4git/ci?style=for-the-badge"></a>
<a href="https://goreportcard.com/report/github.com/bvieira/sv4git"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/bvieira/sv4git?style=for-the-badge"></a>
<a href="https://conventionalcommits.org"><img alt="Software License" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-informational.svg?style=for-the-badge"></a>
</p>
</p>
## Getting Started ## Getting Started
### Installing ### Installing
download the latest release and add the binary on your path - Download the latest release and add the binary on your path
- Optional: Set `SV4GIT_HOME` to define user configs, check [config](#config) for more information.
### Config ### Config
you can config using the environment variables There are 3 config levels when using sv4git: [default](#default), [user](#user), [repository](#repository). All 3 are merged using the follow priority: **repository > user > default**.
| Variable | description | default | To see current config, run:
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| SV4GIT_MAJOR_VERSION_TYPES | types used to bump major version | | ```bash
| SV4GIT_MINOR_VERSION_TYPES | types used to bump minor version | feat | git sv cfg show
| SV4GIT_PATCH_VERSION_TYPES | types used to bump patch version | build,ci,chore,docs,fix,perf,refactor,style,test | ```
| SV4GIT_INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true |
| SV4GIT_BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: | #### Configuration types
| SV4GIT_ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: |
| SV4GIT_TAG_PATTERN | tag version pattern | %d.%d.%d | ##### Default
| SV4GIT_RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features |
| SV4GIT_VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop | To check what is the default configuration, run:
| SV4GIT_COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test |
| SV4GIT_ISSUE_KEY_NAME | metadata key name used on validate commit message hook to enhance footer, if blank footer will not be added | jira | ```bash
| SV4GIT_ISSUE_REGEX | issue id regex, if blank footer will not be added | [A-Z]+-[0-9]+ | git sv cfg default
| SV4GIT_BRANCH_ISSUE_REGEX | regex to extract issue id from branch name, must have 3 groups (prefix, id, posfix), if blank footer will not be added | ^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)? | ```
##### User
To configure define `SV4GIT_HOME` environment variable, eg.:
```bash
SV4GIT_HOME=/home/myuser/.sv4git # myuser is just an example
```
And define the `config.yml` inside it, eg:
```bash
.sv4git
└── config.yml
```
##### Repository
Create a `.sv4git.yml` on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml)
#### Configuration format
```yml
version: "1.0" #config version
versioning: # versioning bump
update-major: [] # commit types used to bump major
update-minor: # commit types used to bump minor
- feat
update-patch: # commit types used to bump patch
- build
- ci
- chore
- docs
- fix
- perf
- refactor
- style
- test
# when type is not present on update rules and is unknown (not mapped on commit message types),
# if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
ignore-unknown: false
tag:
pattern: '%d.%d.%d' # pattern used to create git tag
release-notes:
headers: # headers names for relase notes markdown, to disable a section, just remove the header line
breaking-change: Breaking Changes
feat: Features
fix: Bug Fixes
branches: # git branches config
prefix: ([a-z]+\/)? # prefix used on branch name, should be a regex group
suffix: (-.*)? # suffix used on branch name, should be a regex group
disable-issue: false # set true if there is no need to recover issue id from branch name
skip: # list of branch names ignored on commit message validation
- master
- main
- developer
commit-message:
types: # supported commit types
- build
- ci
- chore
- docs
- feat
- fix
- perf
- refactor
- revert
- style
- test
scope:
# define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid.
# don't forget to add "" on your list if you need to define scopes and keep it optional
values: []
footer:
issue:
key: jira # name used to define an issue on footer metadata
key-synonyms: # supported variations for footer metadata
- Jira
- JIRA
use-hash: false # if false, use :<space> separator, if true, use <space># separator
issue:
regex: '[A-Z]+-[0-9]+' # regex for issue id
```
### Running ### Running
@ -48,7 +146,7 @@ git sv next-version
#### Usage #### Usage
use `--help` or `-h` to get usage information, dont forget that some commands have unique options too use `--help` or `-h` to get usage information, don't forget that some commands have unique options too
```bash ```bash
# sv help # sv help
@ -60,18 +158,19 @@ git-sv rn -h
##### Available commands ##### Available commands
| Variable | description | has options | | Variable | description | has options or subcommands |
| ---------------------------- | ------------------------------------------------------------- | :----------------: | | ---------------------------- | ------------------------------------------------------------- | :------------------------: |
| current-version, cv | get last released version from git | :x: | | config, cfg | show config information | :heavy_check_mark: |
| next-version, nv | generate the next version based on git commit messages | :x: | | current-version, cv | get last released version from git | :x: |
| commit-log, cl | list all commit logs according to range as jsons | :heavy_check_mark: | | next-version, nv | generate the next version based on git commit messages | :x: |
| commit-notes, cn | generate a commit notes according to range | :heavy_check_mark: | | commit-log, cl | list all commit logs according to range as jsons | :heavy_check_mark: |
| release-notes, rn | generate release notes | :heavy_check_mark: | | commit-notes, cn | generate a commit notes according to range | :heavy_check_mark: |
| changelog, cgl | generate changelog | :heavy_check_mark: | | release-notes, rn | generate release notes | :heavy_check_mark: |
| tag, tg | generate tag with version based on git commit messages | :x: | | changelog, cgl | generate changelog | :heavy_check_mark: |
| commit, cmt | execute git commit with convetional commit message helper | :x: | | tag, tg | generate tag with version based on git commit messages | :x: |
| validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: | | commit, cmt | execute git commit with convetional commit message helper | :x: |
| help, h | shows a list of commands or help for one command | :x: | | validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: |
| help, h | shows a list of commands or help for one command | :x: |
##### Use range ##### Use range

View File

@ -1,33 +1,90 @@
package main package main
import ( import (
"errors"
"fmt"
"io/ioutil"
"log" "log"
"os/exec"
"strings"
"sv4git/sv"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"gopkg.in/yaml.v3"
) )
// Config env vars for cli configuration // EnvConfig env vars for cli configuration
type Config struct { type EnvConfig struct {
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""` Home string `envconfig:"SV4GIT_HOME" default:""`
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,chore,docs,fix,perf,refactor,style,test"`
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE:,BREAKING CHANGES:"`
IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira:,JIRA:,Jira:"`
TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"`
CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"`
IssueKeyName string `envconfig:"ISSUE_KEY_NAME" default:"jira"`
IssueRegex string `envconfig:"ISSUE_REGEX" default:"[A-Z]+-[0-9]+"`
BranchIssueRegex string `envconfig:"BRANCH_ISSUE_REGEX" default:"^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"` //TODO breaking change: use issue regex instead of duplicating issue regex
} }
func loadConfig() Config { func loadEnvConfig() EnvConfig {
var c Config var c EnvConfig
err := envconfig.Process("SV4GIT", &c) err := envconfig.Process("", &c)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }
return c return c
} }
// Config cli yaml config
type Config struct {
Version string `yaml:"version"`
Versioning sv.VersioningConfig `yaml:"versioning"`
Tag sv.TagConfig `yaml:"tag"`
ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"`
Branches sv.BranchesConfig `yaml:"branches"`
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"`
}
func getRepoPath() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
out, err := cmd.CombinedOutput()
if err != nil {
return "", errors.New(string(out))
}
return strings.TrimSpace(string(out)), nil
}
func loadConfig(filepath string) (Config, error) {
content, rerr := ioutil.ReadFile(filepath)
if rerr != nil {
return Config{}, rerr
}
var cfg Config
cerr := yaml.Unmarshal(content, &cfg)
if cerr != nil {
return Config{}, fmt.Errorf("could not parse config from path: %s, error: %v", filepath, cerr)
}
return cfg, nil
}
func defaultConfig() Config {
return Config{
Version: "1.0",
Versioning: sv.VersioningConfig{
UpdateMajor: []string{},
UpdateMinor: []string{"feat"},
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"}},
Branches: sv.BranchesConfig{
PrefixRegex: "([a-z]+\\/)?",
SuffixRegex: "(-.*)?",
DisableIssue: false,
Skip: []string{"master", "main", "developer"},
},
CommitMessage: sv.CommitMessageConfig{
Types: []string{"build", "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"},
Scope: sv.CommitMessageScopeConfig{},
Footer: map[string]sv.CommitMessageFooterConfig{
"issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}},
},
Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
},
}
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sv4git/sv" "sv4git/sv"
@ -12,8 +13,32 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
) )
func configDefaultHandler() func(c *cli.Context) error {
cfg := defaultConfig()
return func(c *cli.Context) error {
content, err := yaml.Marshal(&cfg)
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}
}
func configShowHandler(cfg Config) func(c *cli.Context) error {
return func(c *cli.Context) error {
content, err := yaml.Marshal(&cfg)
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}
}
func currentVersionHandler(git sv.Git) func(c *cli.Context) error { func currentVersionHandler(git sv.Git) func(c *cli.Context) error {
return func(c *cli.Context) error { return func(c *cli.Context) error {
describe := git.Describe() describe := git.Describe()
@ -241,12 +266,12 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error { func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error { return func(c *cli.Context) error {
ctype, err := promptType() ctype, err := promptType(cfg.CommitMessage.Types)
if err != nil { if err != nil {
return err return err
} }
scope, err := promptScope() scope, err := promptScope(cfg.CommitMessage.Scope.Values)
if err != nil { if err != nil {
return err return err
} }
@ -273,7 +298,7 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
if err != nil { if err != nil {
return err return err
} }
issue, err := promptIssueID(cfg.IssueKeyName, cfg.IssueRegex, branchIssue) issue, err := promptIssueID(cfg.CommitMessage.IssueFooterConfig().Key, cfg.CommitMessage.Issue.Regex, branchIssue)
if err != nil { if err != nil {
return err return err
} }
@ -290,7 +315,7 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
} }
} }
header, body, footer := messageProcessor.Format(ctype.Type, scope, subject, fullBody.String(), issue, breakingChanges) header, body, footer := messageProcessor.Format(sv.NewCommitMessage(ctype.Type, scope, subject, fullBody.String(), issue, breakingChanges))
err = git.Commit(header, body, footer) err = git.Commit(header, body, footer)
if err != nil { if err != nil {
@ -351,7 +376,7 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
return nil return nil
} }
filepath := fmt.Sprintf("%s/%s", c.String("path"), c.String("file")) filepath := filepath.Join(c.String("path"), c.String("file"))
commitMessage, err := readFile(filepath) commitMessage, err := readFile(filepath)
if err != nil { if err != nil {

View File

@ -3,30 +3,78 @@ package main
import ( import (
"log" "log"
"os" "os"
"path/filepath"
"sv4git/sv" "sv4git/sv"
"github.com/imdario/mergo"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// Version for git-sv // Version for git-sv
var Version = "" var Version = ""
const (
configFilename = "config.yml"
repoConfigFilename = ".sv4git.yml"
)
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
cfg := loadConfig() envCfg := loadEnvConfig()
git := sv.NewGit(cfg.BreakingChangePrefixes, cfg.IssueIDPrefixes, cfg.TagPattern) cfg := defaultConfig()
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags) if envCfg.Home != "" {
if homeCfg, err := loadConfig(filepath.Join(envCfg.Home, configFilename)); err == nil {
if merr := mergo.Merge(&cfg, homeCfg, mergo.WithOverride); merr != nil {
log.Fatal(merr)
}
}
}
repoPath, rerr := getRepoPath()
if rerr != nil {
log.Fatal(rerr)
}
if repoCfg, err := loadConfig(filepath.Join(repoPath, repoConfigFilename)); err == nil {
if merr := mergo.Merge(&cfg, repoCfg, mergo.WithOverride); merr != nil {
log.Fatal(merr)
}
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
}
}
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() outputFormatter := sv.NewOutputFormatter()
messageProcessor := sv.NewMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex, cfg.IssueRegex)
app := cli.NewApp() app := cli.NewApp()
app.Name = "sv" app.Name = "sv"
app.Version = Version app.Version = Version
app.Usage = "semantic version for git" app.Usage = "semantic version for git"
app.Commands = []*cli.Command{ app.Commands = []*cli.Command{
{
Name: "config",
Aliases: []string{"cfg"},
Usage: "cli configuration",
Subcommands: []*cli.Command{
{
Name: "default",
Usage: "show default config",
Action: configDefaultHandler(),
},
{
Name: "show",
Usage: "show current config",
Action: configShowHandler(cfg),
},
},
},
{ {
Name: "current-version", Name: "current-version",
Aliases: []string{"cv"}, Aliases: []string{"cv"},

View File

@ -14,18 +14,28 @@ type commitType struct {
Example string Example string
} }
func promptType() (commitType, error) { func promptType(types []string) (commitType, error) {
items := []commitType{ defaultTypes := map[string]commitType{
{Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"}, "build": {Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"},
{Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"}, "ci": {Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"},
{Type: "chore", Description: "update something without impacting the user", Example: "gitignore"}, "chore": {Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
{Type: "docs", Description: "documentation only changes"}, "docs": {Type: "docs", Description: "documentation only changes"},
{Type: "feat", Description: "a new feature"}, "feat": {Type: "feat", Description: "a new feature"},
{Type: "fix", Description: "a bug fix"}, "fix": {Type: "fix", Description: "a bug fix"},
{Type: "perf", Description: "a code change that improves performance"}, "perf": {Type: "perf", Description: "a code change that improves performance"},
{Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"}, "refactor": {Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"},
{Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"}, "style": {Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"},
{Type: "test", Description: "adding missing tests or correcting existing tests"}, "test": {Type: "test", Description: "adding missing tests or correcting existing tests"},
"revert": {Type: "revert", Description: "revert a single commit"},
}
var items []commitType
for _, t := range types {
if v, exists := defaultTypes[t]; exists {
items = append(items, v)
} else {
items = append(items, commitType{Type: t})
}
} }
template := &promptui.SelectTemplates{ template := &promptui.SelectTemplates{
@ -46,7 +56,14 @@ func promptType() (commitType, error) {
return items[i], nil return items[i], nil
} }
func promptScope() (string, error) { func promptScope(values []string) (string, error) {
if len(values) > 0 {
selected, err := promptSelect("scope", values, nil)
if err != nil {
return "", err
}
return values[selected], nil
}
return promptText("scope", "^[a-z0-9-]*$", "") return promptText("scope", "^[a-z0-9-]*$", "")
} }

2
go.mod
View File

@ -5,6 +5,7 @@ go 1.15
require ( require (
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.1.1
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/imdario/mergo v0.3.11
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect
@ -14,4 +15,5 @@ require (
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
) )

6
go.sum
View File

@ -12,6 +12,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
@ -61,3 +63,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

70
sv/config.go Normal file
View File

@ -0,0 +1,70 @@
package sv
// ==== Message ====
// CommitMessageConfig config a commit message.
type CommitMessageConfig struct {
Types []string `yaml:"types"`
Scope CommitMessageScopeConfig `yaml:"scope"`
Footer map[string]CommitMessageFooterConfig `yaml:"footer"`
Issue CommitMessageIssueConfig `yaml:"issue"`
}
// IssueFooterConfig config for issue.
func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
if v, exists := c.Footer[issueMetadataKey]; exists {
return v
}
return CommitMessageFooterConfig{}
}
// CommitMessageScopeConfig config scope preferences.
type CommitMessageScopeConfig struct {
Values []string `yaml:"values"`
}
// CommitMessageFooterConfig config footer metadata.
type CommitMessageFooterConfig struct {
Key string `yaml:"key"`
KeySynonyms []string `yaml:"key-synonyms"`
UseHash bool `yaml:"use-hash"`
}
// CommitMessageIssueConfig issue preferences.
type CommitMessageIssueConfig struct {
Regex string `yaml:"regex"`
}
// ==== Branches ====
// BranchesConfig branches preferences.
type BranchesConfig struct {
PrefixRegex string `yaml:"prefix"`
SuffixRegex string `yaml:"suffix"`
DisableIssue bool `yaml:"disable-issue"`
Skip []string `yaml:"skip"`
}
// ==== Versioning ====
// VersioningConfig versioning preferences.
type VersioningConfig struct {
UpdateMajor []string `yaml:"update-major"`
UpdateMinor []string `yaml:"update-minor"`
UpdatePatch []string `yaml:"update-patch"`
IgnoreUnknown bool `yaml:"ignore-unknown"`
}
// ==== Tag ====
// TagConfig tag preferences.
type TagConfig struct {
Pattern string `yaml:"pattern"`
}
// ==== Release Notes ====
// ReleaseNotesConfig release notes preferences.
type ReleaseNotesConfig struct {
Headers map[string]string `yaml:"headers"`
}

View File

@ -10,7 +10,7 @@ type releaseNoteTemplateVariables struct {
Version string Version string
Date string Date string
Sections map[string]ReleaseNoteSection Sections map[string]ReleaseNoteSection
BreakingChanges []string BreakingChanges BreakingChangeSection
} }
const ( const (
@ -22,7 +22,7 @@ const (
{{- end}} {{- end}}
` `
rnSectionItem = "- {{if .Scope}}**{{.Scope}}:** {{end}}{{.Subject}} ({{.Hash}}){{if .Metadata.issueid}} ({{.Metadata.issueid}}){{end}}" rnSectionItem = "- {{if .Message.Scope}}**{{.Message.Scope}}:** {{end}}{{.Message.Description}} ({{.Hash}}){{if .Message.Metadata.issue}} ({{.Message.Metadata.issue}}){{end}}"
rnSection = `{{- if .}} rnSection = `{{- if .}}
@ -32,10 +32,10 @@ const (
{{- end}} {{- end}}
{{- end}}` {{- end}}`
rnSectionBreakingChanges = `{{- if .}} rnSectionBreakingChanges = `{{- if ne .Name ""}}
### Breaking Changes ### {{.Name}}
{{range $k,$v := .}} {{range $k,$v := .Messages}}
- {{$v}} - {{$v}}
{{- end}} {{- end}}
{{- end}}` {{- end}}`

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"regexp"
"strings" "strings"
"time" "time"
@ -16,11 +15,6 @@ import (
const ( const (
logSeparator = "##" logSeparator = "##"
endLine = "~~" endLine = "~~"
// BreakingChangesKey key to breaking change metadata
BreakingChangesKey = "breakingchange"
// IssueIDKey key to issue id metadata
IssueIDKey = "issueid"
) )
// Git commands // Git commands
@ -35,13 +29,9 @@ 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"` Hash string `json:"hash,omitempty"`
Type string `json:"type,omitempty"` Message CommitMessage `json:"message,omitempty"`
Scope string `json:"scope,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
} }
// GitTag git tag info // GitTag git tag info
@ -74,15 +64,15 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
// GitImpl git command implementation // GitImpl git command implementation
type GitImpl struct { type GitImpl struct {
messageMetadata map[string][]string messageProcessor MessageProcessor
tagPattern string tagCfg TagConfig
} }
// NewGit constructor // NewGit constructor
func NewGit(breakinChangePrefixes, issueIDPrefixes []string, tagPattern string) *GitImpl { func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
return &GitImpl{ return &GitImpl{
messageMetadata: map[string][]string{BreakingChangesKey: breakinChangePrefixes, IssueIDKey: issueIDPrefixes}, messageProcessor: messageProcessor,
tagPattern: tagPattern, tagCfg: cfg,
} }
} }
@ -119,7 +109,7 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
if err != nil { if err != nil {
return nil, combinedOutputErr(err, out) return nil, combinedOutputErr(err, out)
} }
return parseLogOutput(g.messageMetadata, string(out)), nil return parseLogOutput(g.messageProcessor, string(out)), nil
} }
// Commit runs git commit // Commit runs git commit
@ -132,7 +122,7 @@ func (g GitImpl) Commit(header, body, footer string) error {
// Tag create a git tag // Tag create a git tag
func (g GitImpl) Tag(version semver.Version) error { func (g GitImpl) Tag(version semver.Version) error {
tag := fmt.Sprintf(g.tagPattern, version.Major(), version.Minor(), version.Patch()) tag := fmt.Sprintf(g.tagCfg.Pattern, version.Major(), version.Minor(), version.Patch())
tagMsg := fmt.Sprintf("Version %d.%d.%d", version.Major(), version.Minor(), version.Patch()) tagMsg := fmt.Sprintf("Version %d.%d.%d", version.Major(), version.Minor(), version.Patch())
tagCommand := exec.Command("git", "tag", "-a", tag, "-m", tagMsg) tagCommand := exec.Command("git", "tag", "-a", tag, "-m", tagMsg)
@ -177,61 +167,28 @@ func parseTagsOutput(input string) ([]GitTag, error) {
return result, nil return result, nil
} }
func parseLogOutput(messageMetadata map[string][]string, log string) []GitCommitLog { func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLog {
scanner := bufio.NewScanner(strings.NewReader(log)) scanner := bufio.NewScanner(strings.NewReader(log))
scanner.Split(splitAt([]byte(endLine))) scanner.Split(splitAt([]byte(endLine)))
var logs []GitCommitLog var logs []GitCommitLog
for scanner.Scan() { for scanner.Scan() {
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" { if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
logs = append(logs, parseCommitLog(messageMetadata, text)) logs = append(logs, parseCommitLog(messageProcessor, text))
} }
} }
return logs return logs
} }
func parseCommitLog(messageMetadata map[string][]string, commit string) GitCommitLog { func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog {
content := strings.Split(strings.Trim(commit, "\""), logSeparator) content := strings.Split(strings.Trim(commit, "\""), logSeparator)
commitType, scope, subject := parseCommitLogMessage(content[2])
metadata := make(map[string]string)
for key, prefixes := range messageMetadata {
for _, prefix := range prefixes {
if tagValue := extractTag(prefix, content[3]); tagValue != "" {
metadata[key] = tagValue
break
}
}
}
return GitCommitLog{ return GitCommitLog{
Date: content[0], Date: content[0],
Hash: content[1], Hash: content[1],
Type: commitType, Message: messageProcessor.Parse(content[2], content[3]),
Scope: scope,
Subject: subject,
Body: content[3],
Metadata: metadata,
} }
} }
func parseCommitLogMessage(message string) (string, string, string) {
regex := regexp.MustCompile("([a-z]+)(\\((.*)\\))?: (.*)")
result := regex.FindStringSubmatch(message)
if len(result) != 5 {
return "", "", message
}
return result[1], result[3], strings.TrimSpace(result[4])
}
func extractTag(tag, text string) string {
regex := regexp.MustCompile(tag + " (.*)")
result := regex.FindStringSubmatch(text)
if len(result) < 2 {
return ""
}
return result[1]
}
func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) { func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) { return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
dataLen := len(data) dataLen := len(data)

View File

@ -11,20 +11,31 @@ func version(v string) semver.Version {
return *r return *r
} }
func commitlog(t string, metadata map[string]string) GitCommitLog { func commitlog(ctype string, metadata map[string]string) GitCommitLog {
breaking := false
if _, found := metadata[breakingChangeMetadataKey]; found {
breaking = true
}
return GitCommitLog{ return GitCommitLog{
Type: t, Message: CommitMessage{
Subject: "subject text", Type: ctype,
Metadata: metadata, Description: "subject text",
IsBreakingChange: breaking,
Metadata: metadata,
},
} }
} }
func releaseNote(version *semver.Version, date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote { func releaseNote(version *semver.Version, date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote {
var bchanges BreakingChangeSection
if len(breakingChanges) > 0 {
bchanges = BreakingChangeSection{Name: "Breaking Changes", Messages: breakingChanges}
}
return ReleaseNote{ return ReleaseNote{
Version: version, Version: version,
Date: date.Truncate(time.Minute), Date: date.Truncate(time.Minute),
Sections: sections, Sections: sections,
BreakingChanges: breakingChanges, BreakingChanges: bchanges,
} }
} }

View File

@ -2,12 +2,49 @@ package sv
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
) )
const breakingChangeKey = "BREAKING CHANGE" const (
breakingChangeFooterKey = "BREAKING CHANGE"
breakingChangeMetadataKey = "breaking-change"
issueMetadataKey = "issue"
)
// CommitMessage is a message using conventional commits.
type CommitMessage struct {
Type string `json:"type,omitempty"`
Scope string `json:"scope,omitempty"`
Description string `json:"description,omitempty"`
Body string `json:"body,omitempty"`
IsBreakingChange bool `json:"isBreakingChange,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// NewCommitMessage commit message constructor
func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage {
metadata := make(map[string]string)
if issue != "" {
metadata[issueMetadataKey] = issue
}
if breakingChanges != "" {
metadata[breakingChangeMetadataKey] = breakingChanges
}
return CommitMessage{Type: ctype, Scope: scope, Description: description, Body: body, IsBreakingChange: breakingChanges != "", Metadata: metadata}
}
// Issue return issue from metadata.
func (m CommitMessage) Issue() string {
return m.Metadata[issueMetadataKey]
}
// BreakingMessage return breaking change message from metadata.
func (m CommitMessage) BreakingMessage() string {
return m.Metadata[breakingChangeMetadataKey]
}
// MessageProcessor interface. // MessageProcessor interface.
type MessageProcessor interface { type MessageProcessor interface {
@ -15,49 +52,52 @@ type MessageProcessor interface {
Validate(message string) error Validate(message string) error
Enhance(branch string, message string) (string, error) Enhance(branch string, message string) (string, error)
IssueID(branch string) (string, error) IssueID(branch string) (string, error)
Format(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string) Format(msg CommitMessage) (string, string, string)
Parse(subject, body string) CommitMessage
} }
// NewMessageProcessor MessageProcessorImpl constructor // NewMessageProcessor MessageProcessorImpl constructor
func NewMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex, issueRegex string) *MessageProcessorImpl { func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl {
return &MessageProcessorImpl{ return &MessageProcessorImpl{
skipBranches: skipBranches, messageCfg: mcfg,
supportedTypes: supportedTypes, branchesCfg: bcfg,
issueKeyName: issueKeyName,
branchIssueRegex: branchIssueRegex,
issueRegex: issueRegex,
} }
} }
// MessageProcessorImpl process validate message hook. // MessageProcessorImpl process validate message hook.
type MessageProcessorImpl struct { type MessageProcessorImpl struct {
skipBranches []string messageCfg CommitMessageConfig
supportedTypes []string branchesCfg BranchesConfig
issueKeyName string
branchIssueRegex string
issueRegex string
} }
// SkipBranch check if branch should be ignored. // SkipBranch check if branch should be ignored.
func (p MessageProcessorImpl) SkipBranch(branch string) bool { func (p MessageProcessorImpl) SkipBranch(branch string) bool {
return contains(branch, p.skipBranches) return contains(branch, p.branchesCfg.Skip)
} }
// Validate commit message. // Validate commit message.
func (p MessageProcessorImpl) Validate(message string) error { func (p MessageProcessorImpl) Validate(message string) error {
valid, err := regexp.MatchString("^("+strings.Join(p.supportedTypes, "|")+")(\\(.+\\))?!?: .*$", firstLine(message)) subject, body := splitCommitMessageContent(message)
if err != nil { msg := p.Parse(subject, body)
return err
if !regexp.MustCompile("^[a-z+]+(\\(.+\\))?!?: .+$").MatchString(subject) {
return errors.New("message should be valid according with conventional commits")
} }
if !valid {
return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.supportedTypes) if msg.Type == "" || !contains(msg.Type, p.messageCfg.Types) {
return fmt.Errorf("message type should be one of [%v]", strings.Join(p.messageCfg.Types, ", "))
} }
if len(p.messageCfg.Scope.Values) > 0 && !contains(msg.Scope, p.messageCfg.Scope.Values) {
return fmt.Errorf("message scope should one of [%v]", strings.Join(p.messageCfg.Scope.Values, ", "))
}
return nil return nil
} }
// Enhance add metadata on commit message. // Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) { func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
if p.branchIssueRegex == "" || p.issueKeyName == "" || hasIssueID(message, p.issueKeyName) { if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
return "", nil //enhance disabled return "", nil //enhance disabled
} }
@ -69,7 +109,7 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
return "", fmt.Errorf("could not find issue id using configured regex") return "", fmt.Errorf("could not find issue id using configured regex")
} }
footer := fmt.Sprintf("%s: %s", p.issueKeyName, issue) footer := fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue)
if !hasFooter(message) { if !hasFooter(message) {
return "\n" + footer, nil return "\n" + footer, nil
@ -78,11 +118,12 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
return footer, nil return footer, nil
} }
// IssueID try to extract issue id from branch, return empty if not found // IssueID try to extract issue id from branch, return empty if not found.
func (p MessageProcessorImpl) IssueID(branch string) (string, error) { func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
r, err := regexp.Compile(p.branchIssueRegex) rstr := fmt.Sprintf("^%s(%s)%s$", p.branchesCfg.PrefixRegex, p.messageCfg.Issue.Regex, p.branchesCfg.SuffixRegex)
r, err := regexp.Compile(rstr)
if err != nil { if err != nil {
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error()) return "", fmt.Errorf("could not compile issue regex: %s, error: %v", rstr, err.Error())
} }
groups := r.FindStringSubmatch(branch) groups := r.FindStringSubmatch(branch)
@ -92,32 +133,89 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
return groups[2], nil return groups[2], nil
} }
// Format format commit message to header, body and footer // Format a commit message returning header, body and footer.
func (p MessageProcessorImpl) Format(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string) { func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
var header strings.Builder var header strings.Builder
header.WriteString(ctype) header.WriteString(msg.Type)
if scope != "" { if msg.Scope != "" {
header.WriteString("(" + scope + ")") header.WriteString("(" + msg.Scope + ")")
} }
header.WriteString(": ") header.WriteString(": ")
header.WriteString(subject) header.WriteString(msg.Description)
var footer strings.Builder var footer strings.Builder
if breakingChanges != "" { if msg.BreakingMessage() != "" {
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeKey, breakingChanges)) footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
} }
if issue != "" { if issue, exists := msg.Metadata[issueMetadataKey]; exists {
if footer.Len() > 0 { if footer.Len() > 0 {
footer.WriteString("\n") footer.WriteString("\n")
} }
footer.WriteString(fmt.Sprintf("%s: %s", p.issueKeyName, issue)) if p.messageCfg.IssueFooterConfig().UseHash {
footer.WriteString(fmt.Sprintf("%s #%s", p.messageCfg.IssueFooterConfig().Key, strings.TrimPrefix(issue, "#")))
} else {
footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue))
}
} }
return header.String(), body, footer.String() return header.String(), msg.Body, footer.String()
}
// Parse a commit message.
func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage {
commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject)
metadata := make(map[string]string)
for key, mdCfg := range p.messageCfg.Footer {
prefixes := append([]string{mdCfg.Key}, mdCfg.KeySynonyms...)
for _, prefix := range prefixes {
if tagValue := extractFooterMetadata(prefix, body, mdCfg.UseHash); tagValue != "" {
metadata[key] = tagValue
break
}
}
}
if tagValue := extractFooterMetadata(breakingChangeFooterKey, body, false); tagValue != "" {
metadata[breakingChangeMetadataKey] = tagValue
hasBreakingChange = true
}
return CommitMessage{
Type: commitType,
Scope: scope,
Description: description,
Body: body,
IsBreakingChange: hasBreakingChange,
Metadata: metadata,
}
}
func parseSubjectMessage(message string) (string, string, string, bool) {
regex := regexp.MustCompile("([a-z]+)(\\((.*)\\))?(!)?: (.*)")
result := regex.FindStringSubmatch(message)
if len(result) != 6 {
return "", "", message, false
}
return result[1], result[3], strings.TrimSpace(result[5]), result[4] == "!"
}
func extractFooterMetadata(key, text string, useHash bool) string {
var regex *regexp.Regexp
if useHash {
regex = regexp.MustCompile(key + " (#.*)")
} else {
regex = regexp.MustCompile(key + ": (.*)")
}
result := regex.FindStringSubmatch(text)
if len(result) < 2 {
return ""
}
return result[1]
} }
func hasFooter(message string) bool { func hasFooter(message string) bool {
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeKey + ": .*") r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeFooterKey + ": .*")
scanner := bufio.NewScanner(strings.NewReader(message)) scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0 lines := 0
@ -131,8 +229,13 @@ func hasFooter(message string) bool {
return false return false
} }
func hasIssueID(message, issueKeyName string) bool { func hasIssueID(message string, issueConfig CommitMessageFooterConfig) bool {
r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName)) var r *regexp.Regexp
if issueConfig.UseHash {
r = regexp.MustCompile(fmt.Sprintf("(?m)^%s #.+$", issueConfig.Key))
} else {
r = regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueConfig.Key))
}
return r.MatchString(message) return r.MatchString(message)
} }
@ -145,6 +248,21 @@ func contains(value string, content []string) bool {
return false return false
} }
func firstLine(value string) string { func splitCommitMessageContent(content string) (string, string) {
return strings.Split(value, "\n")[0] scanner := bufio.NewScanner(strings.NewReader(content))
scanner.Scan()
subject := scanner.Text()
var body strings.Builder
first := true
for scanner.Scan() {
if !first {
body.WriteString("\n")
}
body.WriteString(scanner.Text())
first = false
}
return subject, body.String()
} }

View File

@ -1,13 +1,35 @@
package sv package sv
import ( import (
"reflect"
"testing" "testing"
) )
const ( var ccfg = CommitMessageConfig{
branchIssueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?" Types: []string{"feat", "fix"},
issueRegex = "[A-Z]+-[0-9]+" Scope: CommitMessageScopeConfig{},
) Footer: map[string]CommitMessageFooterConfig{
"issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
"refs": {Key: "Refs", UseHash: true},
},
Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}
var ccfgWithScope = CommitMessageConfig{
Types: []string{"feat", "fix"},
Scope: CommitMessageScopeConfig{Values: []string{"", "scope"}},
Footer: map[string]CommitMessageFooterConfig{
"issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
"refs": {Key: "Refs", UseHash: true},
},
Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}
var bcfg = BranchesConfig{
PrefixRegex: "([a-z]+\\/)?",
SuffixRegex: "(-.*)?",
Skip: []string{"develop", "master"},
}
// messages samples start // messages samples start
var fullMessage = `fix: correct minor typos in code var fullMessage = `fix: correct minor typos in code
@ -46,31 +68,33 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
// multiline samples end // multiline samples end
func TestMessageProcessorImpl_Validate(t *testing.T) { func TestMessageProcessorImpl_Validate(t *testing.T) {
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
tests := []struct { tests := []struct {
name string name string
cfg CommitMessageConfig
message string message string
wantErr bool wantErr bool
}{ }{
{"single line valid message", "feat: add something", false}, {"single line valid message", ccfg, "feat: add something", false},
{"single line valid message with scope", "feat(scope): add something", false}, {"single line valid message with scope", ccfg, "feat(scope): add something", false},
{"single line invalid type message", "something: add something", true}, {"single line valid scope from list", ccfgWithScope, "feat(scope): add something", false},
{"single line invalid type message", "feat?: add something", true}, {"single line invalid scope from list", ccfgWithScope, "feat(invalid): add something", true},
{"single line invalid type message", ccfg, "something: add something", true},
{"single line invalid type message", ccfg, "feat?: add something", true},
{"multi line valid message", `feat: add something {"multi line valid message", ccfg, `feat: add something
team: x`, false}, team: x`, false},
{"multi line invalid message", `feat add something {"multi line invalid message", ccfg, `feat add something
team: x`, true}, team: x`, true},
{"support ! for breaking change", "feat!: add something", false}, {"support ! for breaking change", ccfg, "feat!: add something", false},
{"support ! with scope for breaking change", "feat(scope)!: add something", false}, {"support ! with scope for breaking change", ccfg, "feat(scope)!: add something", false},
} }
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 := NewMessageProcessor(tt.cfg, bcfg)
if err := p.Validate(tt.message); (err != nil) != tt.wantErr { if err := p.Validate(tt.message); (err != nil) != tt.wantErr {
t.Errorf("MessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("MessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr)
} }
@ -79,7 +103,7 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
} }
func TestMessageProcessorImpl_Enhance(t *testing.T) { func TestMessageProcessorImpl_Enhance(t *testing.T) {
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex) p := NewMessageProcessor(ccfg, bcfg)
tests := []struct { tests := []struct {
name string name string
@ -112,7 +136,7 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
} }
func TestMessageProcessorImpl_IssueID(t *testing.T) { func TestMessageProcessorImpl_IssueID(t *testing.T) {
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex) p := NewMessageProcessor(ccfg, bcfg)
tests := []struct { tests := []struct {
name string name string
@ -149,89 +173,33 @@ c`
jira: JIRA-123` jira: JIRA-123`
) )
func TestMessageProcessorImpl_Format(t *testing.T) {
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
type args struct {
ctype string
scope string
subject string
body string
issue string
breakingChanges string
}
tests := []struct {
name string
args args
wantHeader string
wantBody string
wantFooter string
}{
{"type and subject", args{"feat", "", "subject", "", "", ""}, "feat: subject", "", ""},
{"type, scope and subject", args{"feat", "scope", "subject", "", "", ""}, "feat(scope): subject", "", ""},
{"type, scope, subject and issue", args{"feat", "scope", "subject", "", "JIRA-123", ""}, "feat(scope): subject", "", "jira: JIRA-123"},
{"type, scope, subject and breaking change", args{"feat", "scope", "subject", "", "", "breaks"}, "feat(scope): subject", "", "BREAKING CHANGE: breaks"},
{"full message", args{"feat", "scope", "subject", multilineBody, "JIRA-123", "breaks"}, "feat(scope): subject", multilineBody, fullFooter},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
header, body, footer := p.Format(tt.args.ctype, tt.args.scope, tt.args.subject, tt.args.body, tt.args.issue, tt.args.breakingChanges)
if header != tt.wantHeader {
t.Errorf("MessageProcessorImpl.Format() header = %v, want %v", header, tt.wantHeader)
}
if body != tt.wantBody {
t.Errorf("MessageProcessorImpl.Format() body = %v, want %v", body, tt.wantBody)
}
if footer != tt.wantFooter {
t.Errorf("MessageProcessorImpl.Format() footer = %v, want %v", footer, tt.wantFooter)
}
})
}
}
func Test_firstLine(t *testing.T) {
tests := []struct {
name string
value string
want string
}{
{"empty string", "", ""},
{"single line string", "single line", "single line"},
{"multi line string", `first line
last line`, "first line"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := firstLine(tt.value); got != tt.want {
t.Errorf("firstLine() = %v, want %v", got, tt.want)
}
})
}
}
func Test_hasIssueID(t *testing.T) { func Test_hasIssueID(t *testing.T) {
cfgColon := CommitMessageFooterConfig{Key: "jira"}
cfgHash := CommitMessageFooterConfig{Key: "jira", UseHash: true}
tests := []struct { tests := []struct {
name string name string
message string message string
issueKeyName string issueCfg CommitMessageFooterConfig
want bool want bool
}{ }{
{"single line without issue", "feat: something", "jira", false}, {"single line without issue", "feat: something", cfgColon, false},
{"multi line without issue", `feat: something {"multi line without issue", `feat: something
yay`, "jira", false}, yay`, cfgColon, false},
{"multi line without jira issue", `feat: something {"multi line without jira issue", `feat: something
jira1: JIRA-123`, "jira", false}, jira1: JIRA-123`, cfgColon, false},
{"multi line with issue", `feat: something {"multi line with issue", `feat: something
jira: JIRA-123`, "jira", true}, jira: JIRA-123`, cfgColon, true},
{"multi line with issue and hash", `feat: something
jira #JIRA-123`, cfgHash, 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) {
if got := hasIssueID(tt.message, tt.issueKeyName); got != tt.want { if got := hasIssueID(tt.message, tt.issueCfg); got != tt.want {
t.Errorf("hasIssueID() = %v, want %v", got, tt.want) t.Errorf("hasIssueID() = %v, want %v", got, tt.want)
} }
}) })
@ -258,3 +226,149 @@ func Test_hasFooter(t *testing.T) {
}) })
} }
} }
// conventional commit tests
var completeBody = `some descriptions
jira: JIRA-123
BREAKING CHANGE: this change breaks everything`
var issueOnlyBody = `some descriptions
jira: JIRA-456`
var issueSynonymsBody = `some descriptions
Jira: JIRA-789`
var hashMetadataBody = `some descriptions
Jira: JIRA-999
Refs #123`
func TestMessageProcessorImpl_Parse(t *testing.T) {
p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
subject string
body string
want CommitMessage
}{
{"simple message", "feat: something awesome", "", CommitMessage{Type: "feat", Scope: "", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
{"message with scope", "feat(scope): something awesome", "", CommitMessage{Type: "feat", Scope: "scope", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
{"unmapped type", "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
{"jira and breaking change metadata", "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueMetadataKey: "JIRA-123", breakingChangeMetadataKey: "this change breaks everything"}}},
{"jira only metadata", "feat: something new", issueOnlyBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-456"}}},
{"jira synonyms metadata", "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-789"}}},
{"breaking change with exclamation mark", "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}},
{"hash metadata", "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := p.Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) {
t.Errorf("MessageProcessorImpl.Parse() = %v, want %v", got, tt.want)
}
})
}
}
func TestMessageProcessorImpl_Format(t *testing.T) {
p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
msg CommitMessage
wantHeader string
wantBody string
wantFooter string
}{
{"simple message", NewCommitMessage("feat", "", "something", "", "", ""), "feat: something", "", ""},
{"with issue", NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira: JIRA-123"},
{"with breaking change", NewCommitMessage("feat", "", "something", "", "", "breaks"), "feat: something", "", "BREAKING CHANGE: breaks"},
{"with scope", NewCommitMessage("feat", "scope", "something", "", "", ""), "feat(scope): something", "", ""},
{"with body", NewCommitMessage("feat", "", "something", "body", "", ""), "feat: something", "body", ""},
{"with multiline body", NewCommitMessage("feat", "", "something", multilineBody, "", ""), "feat: something", multilineBody, ""},
{"full message", NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"), "feat(scope): something", multilineBody, fullFooter},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, got2 := p.Format(tt.msg)
if got != tt.wantHeader {
t.Errorf("MessageProcessorImpl.Format() header got = %v, want %v", got, tt.wantHeader)
}
if got1 != tt.wantBody {
t.Errorf("MessageProcessorImpl.Format() body got = %v, want %v", got1, tt.wantBody)
}
if got2 != tt.wantFooter {
t.Errorf("MessageProcessorImpl.Format() footer got = %v, want %v", got2, tt.wantFooter)
}
})
}
}
var expectedBodyFullMessage = `
see the issue for details
on typos fixed.
Reviewed-by: Z
Refs #133`
func Test_splitCommitMessageContent(t *testing.T) {
tests := []struct {
name string
content string
wantSubject string
wantBody string
}{
{"single line commit", "feat: something", "feat: something", ""},
{"multi line commit", fullMessage, "fix: correct minor typos in code", expectedBodyFullMessage},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := splitCommitMessageContent(tt.content)
if got != tt.wantSubject {
t.Errorf("splitCommitMessageContent() subject got = %v, want %v", got, tt.wantSubject)
}
if got1 != tt.wantBody {
t.Errorf("splitCommitMessageContent() body got1 = [%v], want [%v]", got1, tt.wantBody)
}
})
}
}
//commitType, scope, description, hasBreakingChange
func Test_parseSubjectMessage(t *testing.T) {
tests := []struct {
name string
message string
wantType string
wantScope string
wantDescription string
wantHasBreakingChange bool
}{
{"valid commit", "feat: something", "feat", "", "something", false},
{"valid commit with scope", "feat(scope): something", "feat", "scope", "something", false},
{"valid commit with breaking change", "feat(scope)!: something", "feat", "scope", "something", true},
{"missing description", "feat: ", "feat", "", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctype, scope, description, hasBreakingChange := parseSubjectMessage(tt.message)
if ctype != tt.wantType {
t.Errorf("parseSubjectMessage() type got = %v, want %v", ctype, tt.wantType)
}
if scope != tt.wantScope {
t.Errorf("parseSubjectMessage() scope got = %v, want %v", scope, tt.wantScope)
}
if description != tt.wantDescription {
t.Errorf("parseSubjectMessage() description got = %v, want %v", description, tt.wantDescription)
}
if hasBreakingChange != tt.wantHasBreakingChange {
t.Errorf("parseSubjectMessage() hasBreakingChange got = %v, want %v", hasBreakingChange, tt.wantHasBreakingChange)
}
})
}
}

View File

@ -13,12 +13,12 @@ type ReleaseNoteProcessor interface {
// ReleaseNoteProcessorImpl release note based on commit log. // ReleaseNoteProcessorImpl release note based on commit log.
type ReleaseNoteProcessorImpl struct { type ReleaseNoteProcessorImpl struct {
tags map[string]string cfg ReleaseNotesConfig
} }
// NewReleaseNoteProcessor ReleaseNoteProcessor constructor. // NewReleaseNoteProcessor ReleaseNoteProcessor constructor.
func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl { func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
return &ReleaseNoteProcessorImpl{tags: tags} return &ReleaseNoteProcessorImpl{cfg: cfg}
} }
// Create create a release note based on commits. // Create create a release note based on commits.
@ -26,20 +26,25 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, date time.Time
sections := make(map[string]ReleaseNoteSection) sections := make(map[string]ReleaseNoteSection)
var breakingChanges []string var breakingChanges []string
for _, commit := range commits { for _, commit := range commits {
if name, exists := p.tags[commit.Type]; exists { if name, exists := p.cfg.Headers[commit.Message.Type]; exists {
section, sexists := sections[commit.Type] section, sexists := sections[commit.Message.Type]
if !sexists { if !sexists {
section = ReleaseNoteSection{Name: name} section = ReleaseNoteSection{Name: name}
} }
section.Items = append(section.Items, commit) section.Items = append(section.Items, commit)
sections[commit.Type] = section sections[commit.Message.Type] = section
} }
if value, exists := commit.Metadata[BreakingChangesKey]; exists { if commit.Message.BreakingMessage() != "" {
breakingChanges = append(breakingChanges, value) // TODO: if no message found, should use description instead?
breakingChanges = append(breakingChanges, commit.Message.BreakingMessage())
} }
} }
return ReleaseNote{Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges} var breakingChangeSection BreakingChangeSection
if name, exists := p.cfg.Headers[breakingChangeMetadataKey]; exists && len(breakingChanges) > 0 {
breakingChangeSection = BreakingChangeSection{Name: name, Messages: breakingChanges}
}
return ReleaseNote{Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChangeSection}
} }
// ReleaseNote release note. // ReleaseNote release note.
@ -47,7 +52,13 @@ type ReleaseNote struct {
Version *semver.Version Version *semver.Version
Date time.Time Date time.Time
Sections map[string]ReleaseNoteSection Sections map[string]ReleaseNoteSection
BreakingChanges []string BreakingChanges BreakingChangeSection
}
// BreakingChangeSection breaking change section
type BreakingChangeSection struct {
Name string
Messages []string
} }
// ReleaseNoteSection release note section. // ReleaseNoteSection release note section.

View File

@ -36,13 +36,13 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
name: "breaking changes tag", name: "breaking changes tag",
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breakingchange": "breaks"})}, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breaking-change": "breaks"})},
want: releaseNote(semver.MustParse("1.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"), date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}),
}, },
} }
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(map[string]string{"t1": "Tag 1", "t2": "Tag 2"}) p := NewReleaseNoteProcessor(ReleaseNotesConfig{Headers: map[string]string{"t1": "Tag 1", "t2": "Tag 2", "breaking-change": "Breaking Changes"}})
if got := p.Create(tt.version, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) { if got := p.Create(tt.version, 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)
} }

View File

@ -34,16 +34,18 @@ type SemVerCommitsProcessorImpl struct {
MajorVersionTypes map[string]struct{} MajorVersionTypes map[string]struct{}
MinorVersionTypes map[string]struct{} MinorVersionTypes map[string]struct{}
PatchVersionTypes map[string]struct{} PatchVersionTypes map[string]struct{}
KnownTypes []string
IncludeUnknownTypeAsPatch bool IncludeUnknownTypeAsPatch bool
} }
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor // NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor
func NewSemVerCommitsProcessor(unknownAsPatch bool, majorTypes, minorTypes, patchTypes []string) *SemVerCommitsProcessorImpl { func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitsProcessorImpl {
return &SemVerCommitsProcessorImpl{ return &SemVerCommitsProcessorImpl{
IncludeUnknownTypeAsPatch: unknownAsPatch, IncludeUnknownTypeAsPatch: !vcfg.IgnoreUnknown,
MajorVersionTypes: toMap(majorTypes), MajorVersionTypes: toMap(vcfg.UpdateMajor),
MinorVersionTypes: toMap(minorTypes), MinorVersionTypes: toMap(vcfg.UpdateMinor),
PatchVersionTypes: toMap(patchTypes), PatchVersionTypes: toMap(vcfg.UpdatePatch),
KnownTypes: mcfg.Types,
} }
} }
@ -69,19 +71,19 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version semver.Version, commits
} }
func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType { func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType {
if _, exists := commit.Metadata[BreakingChangesKey]; exists { if commit.Message.IsBreakingChange {
return major return major
} }
if _, exists := p.MajorVersionTypes[commit.Type]; exists { if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists {
return major return major
} }
if _, exists := p.MinorVersionTypes[commit.Type]; exists { if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists {
return minor return minor
} }
if _, exists := p.PatchVersionTypes[commit.Type]; exists { if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
return patch return patch
} }
if p.IncludeUnknownTypeAsPatch { if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch {
return patch return patch
} }
return none return none

View File

@ -9,23 +9,24 @@ import (
func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) { func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
unknownAsPatch bool ignoreUnknown bool
version semver.Version version semver.Version
commits []GitCommitLog commits []GitCommitLog
want semver.Version want semver.Version
}{ }{
{"no update", false, version("0.0.0"), []GitCommitLog{}, version("0.0.0")}, {"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0")},
{"no update on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")}, {"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")},
{"update patch on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")}, {"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{})}, version("0.0.0")},
{"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")},
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1")}, {"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1")},
{"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0")}, {"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0")},
{"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0")}, {"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0")},
{"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("patch", map[string]string{"breakingchange": "break"})}, version("1.0.0")}, {"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")},
} }
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 := NewSemVerCommitsProcessor(tt.unknownAsPatch, []string{"major"}, []string{"minor"}, []string{"patch"}) p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown}, CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
if got := p.NextVersion(tt.version, tt.commits); !reflect.DeepEqual(got, tt.want) { if got := p.NextVersion(tt.version, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want) t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want)
} }