mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-12 15:00:39 +00:00
commit
a07a355164
21
.sv4git.yml
Normal file
21
.sv4git.yml
Normal 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
165
README.md
@ -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
|
||||||
|
|
||||||
|
@ -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]+"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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"},
|
||||||
|
@ -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
2
go.mod
@ -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
6
go.sum
@ -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
70
sv/config.go
Normal 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"`
|
||||||
|
}
|
@ -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}}`
|
||||||
|
75
sv/git.go
75
sv/git.go
@ -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)
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
198
sv/message.go
198
sv/message.go
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
22
sv/semver.go
22
sv/semver.go
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user