mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-10 02:10:38 +00:00
commit
02fce48717
32
README.md
32
README.md
@ -12,20 +12,21 @@ download the latest release and add the binary on your path
|
||||
|
||||
you can config using the environment variables
|
||||
|
||||
| Variable | description | default |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| MAJOR_VERSION_TYPES | types used to bump major version | |
|
||||
| MINOR_VERSION_TYPES | types used to bump minor version | feat |
|
||||
| PATCH_VERSION_TYPES | types used to bump patch version | build,ci,docs,fix,perf,refactor,style,test |
|
||||
| INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true |
|
||||
| BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: |
|
||||
| ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: |
|
||||
| TAG_PATTERN | tag version pattern | %d.%d.%d |
|
||||
| RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features |
|
||||
| VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop |
|
||||
| COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test |
|
||||
| ISSUE_KEY_NAME | metadata key name used on validate commit message hook to enhance footer, if blank footer will not be added | jira |
|
||||
| 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]+)(-.*)? |
|
||||
| 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]+)(-.*)? |
|
||||
|
||||
### Running
|
||||
|
||||
@ -67,8 +68,9 @@ git-sv rn -h
|
||||
| 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: |
|
||||
| help, h | shows a list of commands or help for one command | :x: |
|
||||
|
||||
##### Use validate-commit-message as prepare-commit-msg hook
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
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,docs,fix,perf,refactor,style,test"`
|
||||
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:"`
|
||||
@ -19,12 +19,13 @@ type Config struct {
|
||||
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"`
|
||||
BranchIssueRegex string `envconfig:"BRANCH_ISSUE_REGEX" default:"^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"`
|
||||
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 {
|
||||
var c Config
|
||||
err := envconfig.Process("SV", &c)
|
||||
err := envconfig.Process("SV4GIT", &c)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sv4git/sv"
|
||||
"time"
|
||||
|
||||
@ -188,6 +189,68 @@ 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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scope, err := promptScope()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject, err := promptSubject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fullBody strings.Builder
|
||||
for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fullBody.Len() > 0 {
|
||||
fullBody.WriteString("\n")
|
||||
}
|
||||
if body != "" {
|
||||
fullBody.WriteString(body)
|
||||
}
|
||||
}
|
||||
|
||||
branchIssue, err := messageProcessor.IssueID(git.Branch())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issue, err := promptIssueID(cfg.IssueKeyName, cfg.IssueRegex, branchIssue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasBreakingChanges, err := promptConfirm("has breaking changes?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
breakingChanges := ""
|
||||
if hasBreakingChanges {
|
||||
breakingChanges, err = promptBreakingChanges()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
header, body, footer := messageProcessor.Format(ctype.Type, scope, subject, fullBody.String(), issue, breakingChanges)
|
||||
|
||||
err = git.Commit(header, body, footer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error executing git commit, message: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, formatter sv.OutputFormatter) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
|
||||
@ -231,10 +294,10 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
|
||||
}
|
||||
}
|
||||
|
||||
func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.ValidateMessageProcessor) func(c *cli.Context) error {
|
||||
func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
branch := git.Branch()
|
||||
if validateMessageProcessor.SkipBranch(branch) {
|
||||
if messageProcessor.SkipBranch(branch) {
|
||||
warn("commit message validation skipped, branch in ignore list...")
|
||||
return nil
|
||||
}
|
||||
@ -246,11 +309,11 @@ func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.Valida
|
||||
return fmt.Errorf("failed to read commit message, error: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := validateMessageProcessor.Validate(commitMessage); err != nil {
|
||||
if err := messageProcessor.Validate(commitMessage); err != nil {
|
||||
return fmt.Errorf("invalid commit message, error: %s", err.Error())
|
||||
}
|
||||
|
||||
msg, err := validateMessageProcessor.Enhance(branch, commitMessage)
|
||||
msg, err := messageProcessor.Enhance(branch, commitMessage)
|
||||
if err != nil {
|
||||
warn("could not enhance commit message, %s", err.Error())
|
||||
return nil
|
||||
|
@ -12,13 +12,15 @@ import (
|
||||
var Version = ""
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
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)
|
||||
outputFormatter := sv.NewOutputFormatter()
|
||||
validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex)
|
||||
messageProcessor := sv.NewMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex, cfg.IssueRegex)
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "sv"
|
||||
@ -67,11 +69,17 @@ func main() {
|
||||
Usage: "generate tag with version based on git commit messages",
|
||||
Action: tagHandler(git, semverProcessor),
|
||||
},
|
||||
{
|
||||
Name: "commit",
|
||||
Aliases: []string{"cmt"},
|
||||
Usage: "execute git commit with convetional commit message helper",
|
||||
Action: commitHandler(cfg, git, messageProcessor),
|
||||
},
|
||||
{
|
||||
Name: "validate-commit-message",
|
||||
Aliases: []string{"vcm"},
|
||||
Usage: "use as prepare-commit-message hook to validate message",
|
||||
Action: validateCommitMessageHandler(git, validateMessageProcessor),
|
||||
Action: validateCommitMessageHandler(git, messageProcessor),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "path", Required: true, Usage: "git working directory"},
|
||||
&cli.StringFlag{Name: "file", Required: true, Usage: "name of the file that contains the commit log message"},
|
||||
|
109
cmd/git-sv/prompt.go
Normal file
109
cmd/git-sv/prompt.go
Normal file
@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
)
|
||||
|
||||
type commitType struct {
|
||||
Type string
|
||||
Description string
|
||||
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"},
|
||||
}
|
||||
|
||||
template := &promptui.SelectTemplates{
|
||||
Label: "{{ . }}",
|
||||
Active: "> {{ .Type | white }} - {{ .Description | faint }}",
|
||||
Inactive: " {{ .Type | white }} - {{ .Description | faint }}",
|
||||
Selected: `{{ "type:" | faint }} {{ .Type | white }}`,
|
||||
Details: `
|
||||
{{ "Type:" | faint }} {{ .Type }}
|
||||
{{ "Description:" | faint }} {{ .Description }}
|
||||
{{ "Example:" | faint }} {{ .Example }}`,
|
||||
}
|
||||
|
||||
i, err := promptSelect("type", items, template)
|
||||
if err != nil {
|
||||
return commitType{}, err
|
||||
}
|
||||
return items[i], nil
|
||||
}
|
||||
|
||||
func promptScope() (string, error) {
|
||||
return promptText("scope", "^[a-z0-9-]*$", "")
|
||||
}
|
||||
|
||||
func promptSubject() (string, error) {
|
||||
return promptText("subject", "^[a-z].+$", "")
|
||||
}
|
||||
|
||||
func promptBody() (string, error) {
|
||||
return promptText("body (leave empty to finish)", "^.*$", "")
|
||||
}
|
||||
|
||||
func promptIssueID(issueLabel, issueRegex, defaultValue string) (string, error) {
|
||||
return promptText(issueLabel, "^("+issueRegex+")?$", defaultValue)
|
||||
}
|
||||
|
||||
func promptBreakingChanges() (string, error) {
|
||||
return promptText("Breaking changes description", "[a-z].+", "")
|
||||
}
|
||||
|
||||
func promptSelect(label string, items interface{}, template *promptui.SelectTemplates) (int, error) {
|
||||
if items == nil || reflect.TypeOf(items).Kind() != reflect.Slice {
|
||||
return 0, fmt.Errorf("items %v is not a slice", items)
|
||||
}
|
||||
|
||||
prompt := promptui.Select{
|
||||
Label: label,
|
||||
Size: reflect.ValueOf(items).Len(),
|
||||
Items: items,
|
||||
Templates: template,
|
||||
}
|
||||
|
||||
index, _, err := prompt.Run()
|
||||
return index, err
|
||||
}
|
||||
|
||||
func promptText(label, regex, defaultValue string) (string, error) {
|
||||
validate := func(input string) error {
|
||||
regex := regexp.MustCompile(regex)
|
||||
if !regex.MatchString(input) {
|
||||
return fmt.Errorf("invalid value, expected: %s", regex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
prompt := promptui.Prompt{
|
||||
Label: label,
|
||||
Default: defaultValue,
|
||||
Validate: validate,
|
||||
}
|
||||
|
||||
return prompt.Run()
|
||||
}
|
||||
|
||||
func promptConfirm(label string) (bool, error) {
|
||||
r, err := promptText(label+" [y/n]", "^y|n$", "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return r == "y", nil
|
||||
}
|
3
go.mod
3
go.mod
@ -6,5 +6,6 @@ require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/manifoldco/promptui v0.8.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
)
|
||||
|
31
go.sum
31
go.sum
@ -1,19 +1,44 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
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/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=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
|
||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
10
sv/git.go
10
sv/git.go
@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -26,6 +27,7 @@ const (
|
||||
type Git interface {
|
||||
Describe() string
|
||||
Log(initialTag, endTag string) ([]GitCommitLog, error)
|
||||
Commit(header, body, footer string) error
|
||||
Tag(version semver.Version) error
|
||||
Tags() ([]GitTag, error)
|
||||
Branch() string
|
||||
@ -92,6 +94,14 @@ func (g GitImpl) Log(initialTag, endTag string) ([]GitCommitLog, error) {
|
||||
return parseLogOutput(g.messageMetadata, string(out)), nil
|
||||
}
|
||||
|
||||
// Commit runs git commit
|
||||
func (g GitImpl) Commit(header, body, footer string) error {
|
||||
cmd := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Tag create a git tag
|
||||
func (g GitImpl) Tag(version semver.Version) error {
|
||||
tag := fmt.Sprintf(g.tagPattern, version.Major(), version.Minor(), version.Patch())
|
||||
|
150
sv/message.go
Normal file
150
sv/message.go
Normal file
@ -0,0 +1,150 @@
|
||||
package sv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const breakingChangeKey = "BREAKING CHANGE"
|
||||
|
||||
// MessageProcessor interface.
|
||||
type MessageProcessor interface {
|
||||
SkipBranch(branch string) bool
|
||||
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)
|
||||
}
|
||||
|
||||
// NewMessageProcessor MessageProcessorImpl constructor
|
||||
func NewMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex, issueRegex string) *MessageProcessorImpl {
|
||||
return &MessageProcessorImpl{
|
||||
skipBranches: skipBranches,
|
||||
supportedTypes: supportedTypes,
|
||||
issueKeyName: issueKeyName,
|
||||
branchIssueRegex: branchIssueRegex,
|
||||
issueRegex: issueRegex,
|
||||
}
|
||||
}
|
||||
|
||||
// MessageProcessorImpl process validate message hook.
|
||||
type MessageProcessorImpl struct {
|
||||
skipBranches []string
|
||||
supportedTypes []string
|
||||
issueKeyName string
|
||||
branchIssueRegex string
|
||||
issueRegex string
|
||||
}
|
||||
|
||||
// SkipBranch check if branch should be ignored.
|
||||
func (p MessageProcessorImpl) SkipBranch(branch string) bool {
|
||||
return contains(branch, p.skipBranches)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.supportedTypes)
|
||||
}
|
||||
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) {
|
||||
return "", nil //enhance disabled
|
||||
}
|
||||
|
||||
issue, err := p.IssueID(branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if issue == "" {
|
||||
return "", fmt.Errorf("could not find issue id using configured regex")
|
||||
}
|
||||
|
||||
footer := fmt.Sprintf("%s: %s", p.issueKeyName, issue)
|
||||
|
||||
if !hasFooter(message) {
|
||||
return "\n" + footer, nil
|
||||
}
|
||||
|
||||
return footer, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error())
|
||||
}
|
||||
|
||||
groups := r.FindStringSubmatch(branch)
|
||||
if len(groups) != 4 {
|
||||
return "", nil
|
||||
}
|
||||
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) {
|
||||
var header strings.Builder
|
||||
header.WriteString(ctype)
|
||||
if scope != "" {
|
||||
header.WriteString("(" + scope + ")")
|
||||
}
|
||||
header.WriteString(": ")
|
||||
header.WriteString(subject)
|
||||
|
||||
var footer strings.Builder
|
||||
if breakingChanges != "" {
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeKey, breakingChanges))
|
||||
}
|
||||
if issue != "" {
|
||||
if footer.Len() > 0 {
|
||||
footer.WriteString("\n")
|
||||
}
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", p.issueKeyName, issue))
|
||||
}
|
||||
|
||||
return header.String(), body, footer.String()
|
||||
}
|
||||
|
||||
func hasFooter(message string) bool {
|
||||
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeKey + ": .*")
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||
lines := 0
|
||||
for scanner.Scan() {
|
||||
if lines > 0 && r.MatchString(scanner.Text()) {
|
||||
return true
|
||||
}
|
||||
lines++
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func hasIssueID(message, issueKeyName string) bool {
|
||||
r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName))
|
||||
return r.MatchString(message)
|
||||
}
|
||||
|
||||
func contains(value string, content []string) bool {
|
||||
for _, v := range content {
|
||||
if value == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firstLine(value string) string {
|
||||
return strings.Split(value, "\n")[0]
|
||||
}
|
@ -4,7 +4,10 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var issueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
|
||||
const (
|
||||
branchIssueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
|
||||
issueRegex = "[A-Z]+-[0-9]+"
|
||||
)
|
||||
|
||||
// messages samples start
|
||||
var fullMessage = `fix: correct minor typos in code
|
||||
@ -42,8 +45,8 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
|
||||
|
||||
// multiline samples end
|
||||
|
||||
func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
|
||||
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex)
|
||||
func TestMessageProcessorImpl_Validate(t *testing.T) {
|
||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -69,14 +72,14 @@ func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := p.Validate(tt.message); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateMessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("MessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMessageProcessorImpl_Enhance(t *testing.T) {
|
||||
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex)
|
||||
func TestMessageProcessorImpl_Enhance(t *testing.T) {
|
||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -98,11 +101,89 @@ func TestValidateMessageProcessorImpl_Enhance(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := p.Enhance(tt.branch, tt.message)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateMessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ValidateMessageProcessorImpl.Enhance() = %v, want %v", got, tt.want)
|
||||
t.Errorf("MessageProcessorImpl.Enhance() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageProcessorImpl_IssueID(t *testing.T) {
|
||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
branch string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"simple branch", "JIRA-123", "JIRA-123", false},
|
||||
{"branch with prefix", "feature/JIRA-123", "JIRA-123", false},
|
||||
{"branch with prefix and posfix", "feature/JIRA-123-some-description", "JIRA-123", false},
|
||||
{"branch not found", "feature/wrong123-some-description", "", false},
|
||||
{"empty branch", "", "", false},
|
||||
{"unexpected branch name", "feature /JIRA-123", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := p.IssueID(tt.branch)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("MessageProcessorImpl.IssueID() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
multilineBody = `a
|
||||
b
|
||||
c`
|
||||
fullFooter = `BREAKING CHANGE: breaks
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package sv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateMessageProcessor interface.
|
||||
type ValidateMessageProcessor interface {
|
||||
SkipBranch(branch string) bool
|
||||
Validate(message string) error
|
||||
Enhance(branch string, message string) (string, error)
|
||||
}
|
||||
|
||||
// NewValidateMessageProcessor ValidateMessageProcessorImpl constructor
|
||||
func NewValidateMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex string) *ValidateMessageProcessorImpl {
|
||||
return &ValidateMessageProcessorImpl{
|
||||
skipBranches: skipBranches,
|
||||
supportedTypes: supportedTypes,
|
||||
issueKeyName: issueKeyName,
|
||||
branchIssueRegex: branchIssueRegex,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateMessageProcessorImpl process validate message hook.
|
||||
type ValidateMessageProcessorImpl struct {
|
||||
skipBranches []string
|
||||
supportedTypes []string
|
||||
issueKeyName string
|
||||
branchIssueRegex string
|
||||
}
|
||||
|
||||
// SkipBranch check if branch should be ignored.
|
||||
func (p ValidateMessageProcessorImpl) SkipBranch(branch string) bool {
|
||||
return contains(branch, p.skipBranches)
|
||||
}
|
||||
|
||||
// Validate commit message.
|
||||
func (p ValidateMessageProcessorImpl) Validate(message string) error {
|
||||
valid, err := regexp.MatchString("^("+strings.Join(p.supportedTypes, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.supportedTypes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enhance add metadata on commit message.
|
||||
func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (string, error) {
|
||||
if p.branchIssueRegex == "" || p.issueKeyName == "" || hasIssueID(message, p.issueKeyName) {
|
||||
return "", nil //enhance disabled
|
||||
}
|
||||
|
||||
r, err := regexp.Compile(p.branchIssueRegex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error())
|
||||
}
|
||||
|
||||
groups := r.FindStringSubmatch(branch)
|
||||
if len(groups) != 4 {
|
||||
return "", fmt.Errorf("could not find issue id group with configured regex")
|
||||
}
|
||||
|
||||
footer := fmt.Sprintf("%s: %s", p.issueKeyName, groups[2])
|
||||
|
||||
if !hasFooter(message) {
|
||||
return "\n" + footer, nil
|
||||
}
|
||||
|
||||
return footer, nil
|
||||
}
|
||||
|
||||
func hasFooter(message string) bool {
|
||||
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^BREAKING CHANGE: .*")
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||
lines := 0
|
||||
for scanner.Scan() {
|
||||
if lines > 0 && r.MatchString(scanner.Text()) {
|
||||
return true
|
||||
}
|
||||
lines++
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func hasIssueID(message, issueKeyName string) bool {
|
||||
r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName))
|
||||
return r.MatchString(message)
|
||||
}
|
||||
|
||||
func contains(value string, content []string) bool {
|
||||
for _, v := range content {
|
||||
if value == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firstLine(value string) string {
|
||||
return strings.Split(value, "\n")[0]
|
||||
}
|
Loading…
Reference in New Issue
Block a user