0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-11-25 07:30:38 +00:00

feat: add commit action

This commit is contained in:
Beatriz Vieira 2020-12-01 23:15:51 -03:00
parent 3b0ae34b56
commit 9947c06b72
10 changed files with 300 additions and 33 deletions

View File

@ -12,20 +12,21 @@ download the latest release and add the binary on your path
you can config using the environment variables you can config using the environment variables
| Variable | description | default | | Variable | description | default |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| MAJOR_VERSION_TYPES | types used to bump major version | | | SV4GIT_MAJOR_VERSION_TYPES | types used to bump major version | |
| MINOR_VERSION_TYPES | types used to bump minor version | feat | | SV4GIT_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 | | SV4GIT_PATCH_VERSION_TYPES | types used to bump patch version | build,ci,chore,docs,fix,perf,refactor,style,test |
| INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true | | SV4GIT_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: | | SV4GIT_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: | | SV4GIT_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 | | SV4GIT_TAG_PATTERN | tag version pattern | %d.%d.%d |
| RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features | | SV4GIT_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 | | SV4GIT_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 | | SV4GIT_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 | | SV4GIT_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]+)(-.*)? | | 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 ### Running
@ -67,8 +68,9 @@ git-sv rn -h
| release-notes, rn | generate release notes | :heavy_check_mark: | | release-notes, rn | generate release notes | :heavy_check_mark: |
| changelog, cgl | generate changelog | :heavy_check_mark: | | changelog, cgl | generate changelog | :heavy_check_mark: |
| tag, tg | generate tag with version based on git commit messages | :x: | | 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: | | 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 ##### Use validate-commit-message as prepare-commit-msg hook

View File

@ -19,12 +19,13 @@ type Config struct {
ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"` 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"` 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"` 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 { func loadConfig() Config {
var c Config var c Config
err := envconfig.Process("SV", &c) err := envconfig.Process("SV4GIT", &c)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"sort" "sort"
"strings"
"sv4git/sv" "sv4git/sv"
"time" "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.ValidateMessageProcessor) 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 { 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 { return func(c *cli.Context) error {

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"log" "log"
"os" "os"
"sv4git/sv" "sv4git/sv"
@ -12,13 +13,17 @@ import (
var Version = "" var Version = ""
func main() { func main() {
log.SetFlags(0)
cfg := loadConfig() cfg := loadConfig()
fmt.Printf("%+v\n", cfg)
git := sv.NewGit(cfg.BreakingChangePrefixes, cfg.IssueIDPrefixes, cfg.TagPattern) git := sv.NewGit(cfg.BreakingChangePrefixes, cfg.IssueIDPrefixes, cfg.TagPattern)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes) semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
outputFormatter := sv.NewOutputFormatter() outputFormatter := sv.NewOutputFormatter()
validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex) validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex, cfg.IssueRegex)
app := cli.NewApp() app := cli.NewApp()
app.Name = "sv" app.Name = "sv"
@ -67,6 +72,12 @@ func main() {
Usage: "generate tag with version based on git commit messages", Usage: "generate tag with version based on git commit messages",
Action: tagHandler(git, semverProcessor), Action: tagHandler(git, semverProcessor),
}, },
{
Name: "commit",
Aliases: []string{"cmt"},
Usage: "execute git commit with convetional commit message helper",
Action: commitHandler(cfg, git, validateMessageProcessor),
},
{ {
Name: "validate-commit-message", Name: "validate-commit-message",
Aliases: []string{"vcm"}, Aliases: []string{"vcm"},

109
cmd/git-sv/prompt.go Normal file
View 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
View File

@ -6,5 +6,6 @@ require (
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/kelseyhightower/envconfig v1.4.0 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
View File

@ -1,19 +1,44 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 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 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-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 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/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 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 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/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=

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"regexp" "regexp"
"strings" "strings"
@ -26,6 +27,7 @@ const (
type Git interface { type Git interface {
Describe() string Describe() string
Log(initialTag, endTag string) ([]GitCommitLog, error) Log(initialTag, endTag string) ([]GitCommitLog, error)
Commit(header, body, footer string) error
Tag(version semver.Version) error Tag(version semver.Version) error
Tags() ([]GitTag, error) Tags() ([]GitTag, error)
Branch() string Branch() string
@ -92,6 +94,14 @@ func (g GitImpl) Log(initialTag, endTag string) ([]GitCommitLog, error) {
return parseLogOutput(g.messageMetadata, string(out)), nil 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 // Tag create a git tag
func (g GitImpl) Tag(version semver.Version) error { func (g GitImpl) Tag(version semver.Version) error {
tag := fmt.Sprintf(g.tagPattern, version.Major(), version.Minor(), version.Patch()) tag := fmt.Sprintf(g.tagPattern, version.Major(), version.Minor(), version.Patch())

View File

@ -7,20 +7,25 @@ import (
"strings" "strings"
) )
const breakingChangeKey = "BREAKING CHANGE"
// ValidateMessageProcessor interface. // ValidateMessageProcessor interface.
type ValidateMessageProcessor interface { type ValidateMessageProcessor interface {
SkipBranch(branch string) bool SkipBranch(branch string) bool
Validate(message string) error Validate(message string) error
Enhance(branch string, message string) (string, error) Enhance(branch string, message string) (string, error)
IssueID(branch string) (string, error)
Format(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string)
} }
// NewValidateMessageProcessor ValidateMessageProcessorImpl constructor // NewValidateMessageProcessor ValidateMessageProcessorImpl constructor
func NewValidateMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex string) *ValidateMessageProcessorImpl { func NewValidateMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex, issueRegex string) *ValidateMessageProcessorImpl {
return &ValidateMessageProcessorImpl{ return &ValidateMessageProcessorImpl{
skipBranches: skipBranches, skipBranches: skipBranches,
supportedTypes: supportedTypes, supportedTypes: supportedTypes,
issueKeyName: issueKeyName, issueKeyName: issueKeyName,
branchIssueRegex: branchIssueRegex, branchIssueRegex: branchIssueRegex,
issueRegex: issueRegex,
} }
} }
@ -30,6 +35,7 @@ type ValidateMessageProcessorImpl struct {
supportedTypes []string supportedTypes []string
issueKeyName string issueKeyName string
branchIssueRegex string branchIssueRegex string
issueRegex string
} }
// SkipBranch check if branch should be ignored. // SkipBranch check if branch should be ignored.
@ -55,17 +61,15 @@ func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (st
return "", nil //enhance disabled return "", nil //enhance disabled
} }
r, err := regexp.Compile(p.branchIssueRegex) issue, err := p.IssueID(branch)
if err != nil { if err != nil {
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error()) return "", err
} }
if issue == "" {
groups := r.FindStringSubmatch(branch)
if len(groups) != 4 {
return "", fmt.Errorf("could not find issue id using configured regex") return "", fmt.Errorf("could not find issue id using configured regex")
} }
footer := fmt.Sprintf("%s: %s", p.issueKeyName, groups[2]) footer := fmt.Sprintf("%s: %s", p.issueKeyName, issue)
if !hasFooter(message) { if !hasFooter(message) {
return "\n" + footer, nil return "\n" + footer, nil
@ -74,8 +78,46 @@ func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (st
return footer, nil return footer, nil
} }
// IssueID try to extract issue id from branch, return empty if not found
func (p ValidateMessageProcessorImpl) 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 ValidateMessageProcessorImpl) 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 { func hasFooter(message string) bool {
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^BREAKING CHANGE: .*") r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeKey + ": .*")
scanner := bufio.NewScanner(strings.NewReader(message)) scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0 lines := 0

View File

@ -4,7 +4,10 @@ import (
"testing" "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 // messages samples start
var fullMessage = `fix: correct minor typos in code var fullMessage = `fix: correct minor typos in code
@ -43,7 +46,7 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
// multiline samples end // multiline samples end
func TestValidateMessageProcessorImpl_Validate(t *testing.T) { func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex) p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
tests := []struct { tests := []struct {
name string name string
@ -76,7 +79,7 @@ func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
} }
func TestValidateMessageProcessorImpl_Enhance(t *testing.T) { func TestValidateMessageProcessorImpl_Enhance(t *testing.T) {
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex) p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
tests := []struct { tests := []struct {
name string name string