mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-21 12:00:40 +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
|
||||
|
||||
Semantic version for git
|
||||
<p align="center">
|
||||
<h1 align="center">sv4git</h1>
|
||||
<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
|
||||
|
||||
### 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
|
||||
|
||||
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 |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| SV4GIT_MAJOR_VERSION_TYPES | types used to bump major version | |
|
||||
| SV4GIT_MINOR_VERSION_TYPES | types used to bump minor version | feat |
|
||||
| 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: |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| SV4GIT_ISSUE_REGEX | issue id regex, if blank footer will not be added | [A-Z]+-[0-9]+ |
|
||||
| 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]+)(-.*)? |
|
||||
To see current config, run:
|
||||
|
||||
```bash
|
||||
git sv cfg show
|
||||
```
|
||||
|
||||
#### Configuration types
|
||||
|
||||
##### Default
|
||||
|
||||
To check what is the default configuration, run:
|
||||
|
||||
```bash
|
||||
git sv cfg default
|
||||
```
|
||||
|
||||
##### 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
|
||||
|
||||
@ -48,7 +146,7 @@ git sv next-version
|
||||
|
||||
#### 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
|
||||
# sv help
|
||||
@ -60,18 +158,19 @@ git-sv rn -h
|
||||
|
||||
##### Available commands
|
||||
|
||||
| Variable | description | has options |
|
||||
| ---------------------------- | ------------------------------------------------------------- | :----------------: |
|
||||
| current-version, cv | get last released version from git | :x: |
|
||||
| next-version, nv | generate the next version based on git commit messages | :x: |
|
||||
| commit-log, cl | list all commit logs according to range as jsons | :heavy_check_mark: |
|
||||
| commit-notes, cn | generate a commit notes according to range | :heavy_check_mark: |
|
||||
| release-notes, rn | generate release notes | :heavy_check_mark: |
|
||||
| changelog, cgl | generate changelog | :heavy_check_mark: |
|
||||
| tag, tg | generate tag with version based on git commit messages | :x: |
|
||||
| commit, cmt | execute git commit with convetional commit message helper | :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: |
|
||||
| Variable | description | has options or subcommands |
|
||||
| ---------------------------- | ------------------------------------------------------------- | :------------------------: |
|
||||
| config, cfg | show config information | :heavy_check_mark: |
|
||||
| current-version, cv | get last released version from git | :x: |
|
||||
| next-version, nv | generate the next version based on git commit messages | :x: |
|
||||
| commit-log, cl | list all commit logs according to range as jsons | :heavy_check_mark: |
|
||||
| commit-notes, cn | generate a commit notes according to range | :heavy_check_mark: |
|
||||
| release-notes, rn | generate release notes | :heavy_check_mark: |
|
||||
| changelog, cgl | generate changelog | :heavy_check_mark: |
|
||||
| tag, tg | generate tag with version based on git commit messages | :x: |
|
||||
| commit, cmt | execute git commit with convetional commit message helper | :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
|
||||
|
||||
|
@ -1,33 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sv4git/sv"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config env vars for cli configuration
|
||||
type Config struct {
|
||||
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" 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
|
||||
// EnvConfig env vars for cli configuration
|
||||
type EnvConfig struct {
|
||||
Home string `envconfig:"SV4GIT_HOME" default:""`
|
||||
}
|
||||
|
||||
func loadConfig() Config {
|
||||
var c Config
|
||||
err := envconfig.Process("SV4GIT", &c)
|
||||
func loadEnvConfig() EnvConfig {
|
||||
var c EnvConfig
|
||||
err := envconfig.Process("", &c)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sv4git/sv"
|
||||
@ -12,8 +13,32 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"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 {
|
||||
return func(c *cli.Context) error {
|
||||
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 {
|
||||
return func(c *cli.Context) error {
|
||||
|
||||
ctype, err := promptType()
|
||||
ctype, err := promptType(cfg.CommitMessage.Types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scope, err := promptScope()
|
||||
scope, err := promptScope(cfg.CommitMessage.Scope.Values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -273,7 +298,7 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
@ -351,7 +376,7 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -3,30 +3,78 @@ package main
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sv4git/sv"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version for git-sv
|
||||
var Version = ""
|
||||
|
||||
const (
|
||||
configFilename = "config.yml"
|
||||
repoConfigFilename = ".sv4git.yml"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
cfg := loadConfig()
|
||||
envCfg := loadEnvConfig()
|
||||
|
||||
git := sv.NewGit(cfg.BreakingChangePrefixes, cfg.IssueIDPrefixes, cfg.TagPattern)
|
||||
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
|
||||
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
|
||||
cfg := defaultConfig()
|
||||
|
||||
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()
|
||||
messageProcessor := sv.NewMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex, cfg.IssueRegex)
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "sv"
|
||||
app.Version = Version
|
||||
app.Usage = "semantic version for git"
|
||||
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",
|
||||
Aliases: []string{"cv"},
|
||||
|
@ -14,18 +14,28 @@ type commitType struct {
|
||||
Example string
|
||||
}
|
||||
|
||||
func promptType() (commitType, error) {
|
||||
items := []commitType{
|
||||
{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"},
|
||||
{Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
|
||||
{Type: "docs", Description: "documentation only changes"},
|
||||
{Type: "feat", Description: "a new feature"},
|
||||
{Type: "fix", Description: "a bug fix"},
|
||||
{Type: "perf", Description: "a code change that improves performance"},
|
||||
{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"},
|
||||
{Type: "test", Description: "adding missing tests or correcting existing tests"},
|
||||
func promptType(types []string) (commitType, error) {
|
||||
defaultTypes := map[string]commitType{
|
||||
"build": {Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"},
|
||||
"ci": {Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"},
|
||||
"chore": {Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
|
||||
"docs": {Type: "docs", Description: "documentation only changes"},
|
||||
"feat": {Type: "feat", Description: "a new feature"},
|
||||
"fix": {Type: "fix", Description: "a bug fix"},
|
||||
"perf": {Type: "perf", Description: "a code change that improves performance"},
|
||||
"refactor": {Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"},
|
||||
"style": {Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"},
|
||||
"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{
|
||||
@ -46,7 +56,14 @@ func promptType() (commitType, error) {
|
||||
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-]*$", "")
|
||||
}
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -5,6 +5,7 @@ go 1.15
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
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/kr/text v0.2.0 // indirect
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
@ -14,4 +15,5 @@ require (
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // 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/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
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/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
|
||||
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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
|
||||
Date string
|
||||
Sections map[string]ReleaseNoteSection
|
||||
BreakingChanges []string
|
||||
BreakingChanges BreakingChangeSection
|
||||
}
|
||||
|
||||
const (
|
||||
@ -22,7 +22,7 @@ const (
|
||||
{{- 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 .}}
|
||||
|
||||
@ -32,10 +32,10 @@ const (
|
||||
{{- end}}
|
||||
{{- end}}`
|
||||
|
||||
rnSectionBreakingChanges = `{{- if .}}
|
||||
rnSectionBreakingChanges = `{{- if ne .Name ""}}
|
||||
|
||||
### Breaking Changes
|
||||
{{range $k,$v := .}}
|
||||
### {{.Name}}
|
||||
{{range $k,$v := .Messages}}
|
||||
- {{$v}}
|
||||
{{- end}}
|
||||
{{- end}}`
|
||||
|
75
sv/git.go
75
sv/git.go
@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -16,11 +15,6 @@ import (
|
||||
const (
|
||||
logSeparator = "##"
|
||||
endLine = "~~"
|
||||
|
||||
// BreakingChangesKey key to breaking change metadata
|
||||
BreakingChangesKey = "breakingchange"
|
||||
// IssueIDKey key to issue id metadata
|
||||
IssueIDKey = "issueid"
|
||||
)
|
||||
|
||||
// Git commands
|
||||
@ -35,13 +29,9 @@ type Git interface {
|
||||
|
||||
// GitCommitLog description of a single commit log
|
||||
type GitCommitLog struct {
|
||||
Date string `json:"date,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Message CommitMessage `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// GitTag git tag info
|
||||
@ -74,15 +64,15 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
|
||||
|
||||
// GitImpl git command implementation
|
||||
type GitImpl struct {
|
||||
messageMetadata map[string][]string
|
||||
tagPattern string
|
||||
messageProcessor MessageProcessor
|
||||
tagCfg TagConfig
|
||||
}
|
||||
|
||||
// NewGit constructor
|
||||
func NewGit(breakinChangePrefixes, issueIDPrefixes []string, tagPattern string) *GitImpl {
|
||||
func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
|
||||
return &GitImpl{
|
||||
messageMetadata: map[string][]string{BreakingChangesKey: breakinChangePrefixes, IssueIDKey: issueIDPrefixes},
|
||||
tagPattern: tagPattern,
|
||||
messageProcessor: messageProcessor,
|
||||
tagCfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,7 +109,7 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
|
||||
if err != nil {
|
||||
return nil, combinedOutputErr(err, out)
|
||||
}
|
||||
return parseLogOutput(g.messageMetadata, string(out)), nil
|
||||
return parseLogOutput(g.messageProcessor, string(out)), nil
|
||||
}
|
||||
|
||||
// Commit runs git commit
|
||||
@ -132,7 +122,7 @@ func (g GitImpl) Commit(header, body, footer string) error {
|
||||
|
||||
// Tag create a git tag
|
||||
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())
|
||||
|
||||
tagCommand := exec.Command("git", "tag", "-a", tag, "-m", tagMsg)
|
||||
@ -177,61 +167,28 @@ func parseTagsOutput(input string) ([]GitTag, error) {
|
||||
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.Split(splitAt([]byte(endLine)))
|
||||
var logs []GitCommitLog
|
||||
for scanner.Scan() {
|
||||
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
|
||||
logs = append(logs, parseCommitLog(messageMetadata, text))
|
||||
logs = append(logs, parseCommitLog(messageProcessor, text))
|
||||
}
|
||||
}
|
||||
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)
|
||||
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{
|
||||
Date: content[0],
|
||||
Hash: content[1],
|
||||
Type: commitType,
|
||||
Scope: scope,
|
||||
Subject: subject,
|
||||
Body: content[3],
|
||||
Metadata: metadata,
|
||||
Date: content[0],
|
||||
Hash: content[1],
|
||||
Message: messageProcessor.Parse(content[2], content[3]),
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
dataLen := len(data)
|
||||
|
@ -11,20 +11,31 @@ func version(v string) semver.Version {
|
||||
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{
|
||||
Type: t,
|
||||
Subject: "subject text",
|
||||
Metadata: metadata,
|
||||
Message: CommitMessage{
|
||||
Type: ctype,
|
||||
Description: "subject text",
|
||||
IsBreakingChange: breaking,
|
||||
Metadata: metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
Version: version,
|
||||
Date: date.Truncate(time.Minute),
|
||||
Sections: sections,
|
||||
BreakingChanges: breakingChanges,
|
||||
BreakingChanges: bchanges,
|
||||
}
|
||||
}
|
||||
|
||||
|
198
sv/message.go
198
sv/message.go
@ -2,12 +2,49 @@ package sv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"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.
|
||||
type MessageProcessor interface {
|
||||
@ -15,49 +52,52 @@ type MessageProcessor interface {
|
||||
Validate(message string) error
|
||||
Enhance(branch string, message 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
|
||||
func NewMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex, issueRegex string) *MessageProcessorImpl {
|
||||
func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl {
|
||||
return &MessageProcessorImpl{
|
||||
skipBranches: skipBranches,
|
||||
supportedTypes: supportedTypes,
|
||||
issueKeyName: issueKeyName,
|
||||
branchIssueRegex: branchIssueRegex,
|
||||
issueRegex: issueRegex,
|
||||
messageCfg: mcfg,
|
||||
branchesCfg: bcfg,
|
||||
}
|
||||
}
|
||||
|
||||
// MessageProcessorImpl process validate message hook.
|
||||
type MessageProcessorImpl struct {
|
||||
skipBranches []string
|
||||
supportedTypes []string
|
||||
issueKeyName string
|
||||
branchIssueRegex string
|
||||
issueRegex string
|
||||
messageCfg CommitMessageConfig
|
||||
branchesCfg BranchesConfig
|
||||
}
|
||||
|
||||
// SkipBranch check if branch should be ignored.
|
||||
func (p MessageProcessorImpl) SkipBranch(branch string) bool {
|
||||
return contains(branch, p.skipBranches)
|
||||
return contains(branch, p.branchesCfg.Skip)
|
||||
}
|
||||
|
||||
// Validate commit message.
|
||||
func (p MessageProcessorImpl) Validate(message string) error {
|
||||
valid, err := regexp.MatchString("^("+strings.Join(p.supportedTypes, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
|
||||
if err != nil {
|
||||
return err
|
||||
subject, body := splitCommitMessageContent(message)
|
||||
msg := p.Parse(subject, body)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Enhance add metadata on commit message.
|
||||
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
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
footer := fmt.Sprintf("%s: %s", p.issueKeyName, issue)
|
||||
footer := fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue)
|
||||
|
||||
if !hasFooter(message) {
|
||||
return "\n" + footer, nil
|
||||
@ -78,11 +118,12 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
|
||||
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) {
|
||||
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 {
|
||||
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)
|
||||
@ -92,32 +133,89 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
|
||||
return groups[2], nil
|
||||
}
|
||||
|
||||
// Format format commit message to header, body and footer
|
||||
func (p MessageProcessorImpl) Format(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string) {
|
||||
// Format a commit message returning header, body and footer.
|
||||
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
|
||||
var header strings.Builder
|
||||
header.WriteString(ctype)
|
||||
if scope != "" {
|
||||
header.WriteString("(" + scope + ")")
|
||||
header.WriteString(msg.Type)
|
||||
if msg.Scope != "" {
|
||||
header.WriteString("(" + msg.Scope + ")")
|
||||
}
|
||||
header.WriteString(": ")
|
||||
header.WriteString(subject)
|
||||
header.WriteString(msg.Description)
|
||||
|
||||
var footer strings.Builder
|
||||
if breakingChanges != "" {
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeKey, breakingChanges))
|
||||
if msg.BreakingMessage() != "" {
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
|
||||
}
|
||||
if issue != "" {
|
||||
if issue, exists := msg.Metadata[issueMetadataKey]; exists {
|
||||
if footer.Len() > 0 {
|
||||
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 {
|
||||
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))
|
||||
lines := 0
|
||||
@ -131,8 +229,13 @@ func hasFooter(message string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func hasIssueID(message, issueKeyName string) bool {
|
||||
r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName))
|
||||
func hasIssueID(message string, issueConfig CommitMessageFooterConfig) bool {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -145,6 +248,21 @@ func contains(value string, content []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func firstLine(value string) string {
|
||||
return strings.Split(value, "\n")[0]
|
||||
func splitCommitMessageContent(content string) (string, string) {
|
||||
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
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
branchIssueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
|
||||
issueRegex = "[A-Z]+-[0-9]+"
|
||||
)
|
||||
var ccfg = CommitMessageConfig{
|
||||
Types: []string{"feat", "fix"},
|
||||
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
|
||||
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
|
||||
|
||||
func TestMessageProcessorImpl_Validate(t *testing.T) {
|
||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg CommitMessageConfig
|
||||
message string
|
||||
wantErr bool
|
||||
}{
|
||||
{"single line valid message", "feat: add something", false},
|
||||
{"single line valid message with scope", "feat(scope): add something", false},
|
||||
{"single line invalid type message", "something: add something", true},
|
||||
{"single line invalid type message", "feat?: add something", true},
|
||||
{"single line valid message", ccfg, "feat: add something", false},
|
||||
{"single line valid message with scope", ccfg, "feat(scope): add something", false},
|
||||
{"single line valid scope from list", ccfgWithScope, "feat(scope): add something", false},
|
||||
{"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},
|
||||
|
||||
{"multi line invalid message", `feat add something
|
||||
{"multi line invalid message", ccfg, `feat add something
|
||||
|
||||
team: x`, true},
|
||||
|
||||
{"support ! for breaking change", "feat!: add something", false},
|
||||
{"support ! with scope for breaking change", "feat(scope)!: add something", false},
|
||||
{"support ! for breaking change", ccfg, "feat!: add something", false},
|
||||
{"support ! with scope for breaking change", ccfg, "feat(scope)!: add something", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewMessageProcessor(tt.cfg, bcfg)
|
||||
if err := p.Validate(tt.message); (err != nil) != 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) {
|
||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
||||
p := NewMessageProcessor(ccfg, bcfg)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -112,7 +136,7 @@ func TestMessageProcessorImpl_Enhance(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 {
|
||||
name string
|
||||
@ -149,89 +173,33 @@ c`
|
||||
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) {
|
||||
cfgColon := CommitMessageFooterConfig{Key: "jira"}
|
||||
cfgHash := CommitMessageFooterConfig{Key: "jira", UseHash: true}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message string
|
||||
issueKeyName string
|
||||
want bool
|
||||
name string
|
||||
message string
|
||||
issueCfg CommitMessageFooterConfig
|
||||
want bool
|
||||
}{
|
||||
{"single line without issue", "feat: something", "jira", false},
|
||||
{"single line without issue", "feat: something", cfgColon, false},
|
||||
{"multi line without issue", `feat: something
|
||||
|
||||
yay`, "jira", false},
|
||||
yay`, cfgColon, false},
|
||||
{"multi line without jira issue", `feat: something
|
||||
|
||||
jira1: JIRA-123`, "jira", false},
|
||||
jira1: JIRA-123`, cfgColon, false},
|
||||
{"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 {
|
||||
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)
|
||||
}
|
||||
})
|
||||
@ -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.
|
||||
type ReleaseNoteProcessorImpl struct {
|
||||
tags map[string]string
|
||||
cfg ReleaseNotesConfig
|
||||
}
|
||||
|
||||
// NewReleaseNoteProcessor ReleaseNoteProcessor constructor.
|
||||
func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl {
|
||||
return &ReleaseNoteProcessorImpl{tags: tags}
|
||||
func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
|
||||
return &ReleaseNoteProcessorImpl{cfg: cfg}
|
||||
}
|
||||
|
||||
// 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)
|
||||
var breakingChanges []string
|
||||
for _, commit := range commits {
|
||||
if name, exists := p.tags[commit.Type]; exists {
|
||||
section, sexists := sections[commit.Type]
|
||||
if name, exists := p.cfg.Headers[commit.Message.Type]; exists {
|
||||
section, sexists := sections[commit.Message.Type]
|
||||
if !sexists {
|
||||
section = ReleaseNoteSection{Name: name}
|
||||
}
|
||||
section.Items = append(section.Items, commit)
|
||||
sections[commit.Type] = section
|
||||
sections[commit.Message.Type] = section
|
||||
}
|
||||
if value, exists := commit.Metadata[BreakingChangesKey]; exists {
|
||||
breakingChanges = append(breakingChanges, value)
|
||||
if commit.Message.BreakingMessage() != "" {
|
||||
// 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.
|
||||
@ -47,7 +52,13 @@ type ReleaseNote struct {
|
||||
Version *semver.Version
|
||||
Date time.Time
|
||||
Sections map[string]ReleaseNoteSection
|
||||
BreakingChanges []string
|
||||
BreakingChanges BreakingChangeSection
|
||||
}
|
||||
|
||||
// BreakingChangeSection breaking change section
|
||||
type BreakingChangeSection struct {
|
||||
Name string
|
||||
Messages []string
|
||||
}
|
||||
|
||||
// ReleaseNoteSection release note section.
|
||||
|
@ -36,13 +36,13 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
|
||||
name: "breaking changes tag",
|
||||
version: semver.MustParse("1.0.0"),
|
||||
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"}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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) {
|
||||
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{}
|
||||
MinorVersionTypes map[string]struct{}
|
||||
PatchVersionTypes map[string]struct{}
|
||||
KnownTypes []string
|
||||
IncludeUnknownTypeAsPatch bool
|
||||
}
|
||||
|
||||
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor
|
||||
func NewSemVerCommitsProcessor(unknownAsPatch bool, majorTypes, minorTypes, patchTypes []string) *SemVerCommitsProcessorImpl {
|
||||
func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitsProcessorImpl {
|
||||
return &SemVerCommitsProcessorImpl{
|
||||
IncludeUnknownTypeAsPatch: unknownAsPatch,
|
||||
MajorVersionTypes: toMap(majorTypes),
|
||||
MinorVersionTypes: toMap(minorTypes),
|
||||
PatchVersionTypes: toMap(patchTypes),
|
||||
IncludeUnknownTypeAsPatch: !vcfg.IgnoreUnknown,
|
||||
MajorVersionTypes: toMap(vcfg.UpdateMajor),
|
||||
MinorVersionTypes: toMap(vcfg.UpdateMinor),
|
||||
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 {
|
||||
if _, exists := commit.Metadata[BreakingChangesKey]; exists {
|
||||
if commit.Message.IsBreakingChange {
|
||||
return major
|
||||
}
|
||||
if _, exists := p.MajorVersionTypes[commit.Type]; exists {
|
||||
if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists {
|
||||
return major
|
||||
}
|
||||
if _, exists := p.MinorVersionTypes[commit.Type]; exists {
|
||||
if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists {
|
||||
return minor
|
||||
}
|
||||
if _, exists := p.PatchVersionTypes[commit.Type]; exists {
|
||||
if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
|
||||
return patch
|
||||
}
|
||||
if p.IncludeUnknownTypeAsPatch {
|
||||
if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch {
|
||||
return patch
|
||||
}
|
||||
return none
|
||||
|
@ -9,23 +9,24 @@ import (
|
||||
|
||||
func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
unknownAsPatch bool
|
||||
version semver.Version
|
||||
commits []GitCommitLog
|
||||
want semver.Version
|
||||
name string
|
||||
ignoreUnknown bool
|
||||
version semver.Version
|
||||
commits []GitCommitLog
|
||||
want semver.Version
|
||||
}{
|
||||
{"no update", false, 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")},
|
||||
{"update patch on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")},
|
||||
{"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0")},
|
||||
{"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")},
|
||||
{"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")},
|
||||
{"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")},
|
||||
{"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 {
|
||||
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) {
|
||||
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user