From 005d4ab939bed2ccb3e48679bdb4fe69898205ea Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Thu, 16 Jul 2020 20:24:19 -0300 Subject: [PATCH 1/7] build: update urfave cli to 2.0.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 14bdc18..3d7c5d6 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,5 @@ 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.1.1 + github.com/urfave/cli/v2 v2.2.0 ) diff --git a/go.sum b/go.sum index c3152d9..3894527 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,7 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 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.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= -github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 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= From 073deacecabd3bc924e75250e0ca9e87338ae1b1 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Thu, 16 Jul 2020 20:25:19 -0300 Subject: [PATCH 2/7] build: add sv version on make release-all --- Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 862c50e..368caae 100644 --- a/Makefile +++ b/Makefile @@ -50,9 +50,10 @@ release: make build @zip -j bin/git-sv_$(VERSION)_$(BUILDOS)_$(BUILDARCH).zip bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) -## release-all: prepare linux, darwin and windows binary for release +## release-all: prepare linux, darwin and windows binary for release (requires sv4git) release-all: @rm -rf bin - BUILDOS=linux make release - BUILDOS=darwin make release - BUILDOS=windows make release + + VERSION=$(shell git sv nv) BUILDOS=linux make release + VERSION=$(shell git sv nv) BUILDOS=darwin make release + VERSION=$(shell git sv nv) BUILDOS=windows make release From 943487fae810af98fe893e65d7b96d6f64db69cf Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Thu, 27 Aug 2020 22:30:56 -0300 Subject: [PATCH 3/7] refact: remove redundant type from slice --- sv/git_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sv/git_test.go b/sv/git_test.go index 7b96394..8771d57 100644 --- a/sv/git_test.go +++ b/sv/git_test.go @@ -13,8 +13,8 @@ func Test_parseTagsOutput(t *testing.T) { want []GitTag wantErr bool }{ - {"with date", "2020-05-01 18:00:00 -0300#1.0.0", []GitTag{GitTag{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}}, false}, - {"without date", "#1.0.0", []GitTag{GitTag{Name: "1.0.0", Date: time.Time{}}}, false}, + {"with date", "2020-05-01 18:00:00 -0300#1.0.0", []GitTag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}}, false}, + {"without date", "#1.0.0", []GitTag{{Name: "1.0.0", Date: time.Time{}}}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 67cd90c762a8b11649e263a395c7ccea5791a1a9 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Thu, 27 Aug 2020 22:57:55 -0300 Subject: [PATCH 4/7] feat: add validate-commit-message action --- cmd/git-sv/config.go | 18 ++++++----- cmd/git-sv/handlers.go | 54 ++++++++++++++++++++++++++++++++ cmd/git-sv/log.go | 7 +++++ cmd/git-sv/main.go | 12 +++++++ sv/git.go | 11 +++++++ sv/validatemessage.go | 64 ++++++++++++++++++++++++++++++++++++++ sv/validatemessage_test.go | 60 +++++++++++++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 cmd/git-sv/log.go create mode 100644 sv/validatemessage.go create mode 100644 sv/validatemessage_test.go diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index c71b4e7..f18f151 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -8,14 +8,16 @@ import ( // 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,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"` + 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"` + 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"` } func loadConfig() Config { diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index 6e2473c..7c53242 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -3,6 +3,8 @@ package main import ( "encoding/json" "fmt" + "io/ioutil" + "os" "sort" "sv4git/sv" "time" @@ -228,3 +230,55 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP return nil } } + +func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.ValidateMessageProcessor) func(c *cli.Context) error { + return func(c *cli.Context) error { + branch := git.Branch() + if validateMessageProcessor.SkipBranch(branch) { + warn("commit message validation skipped, branch in ignore list...") + return nil + } + + filepath := fmt.Sprintf("%s/%s", c.String("path"), c.String("file")) + + commitMessage, err := readFile(filepath) + if err != nil { + return fmt.Errorf("failed to read commit message, error: %s", err.Error()) + } + + if err := validateMessageProcessor.Validate(commitMessage); err != nil { + return fmt.Errorf("invalid commit message, error: %s", err.Error()) + } + + msg, err := validateMessageProcessor.Enhance(branch, commitMessage) + if err != nil { + warn("could not enhance commit message, %s", err.Error()) + return nil + } + + if err := appendOnFile(msg, filepath); err != nil { + return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error()) + } + + return nil + } +} + +func readFile(filepath string) (string, error) { + f, err := ioutil.ReadFile(filepath) + if err != nil { + return "", err + } + return string(f), nil +} + +func appendOnFile(message, filepath string) error { + f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(message) + return err +} diff --git a/cmd/git-sv/log.go b/cmd/git-sv/log.go new file mode 100644 index 0000000..932f7fb --- /dev/null +++ b/cmd/git-sv/log.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func warn(format string, values ...interface{}) { + fmt.Printf("WARN: "+format+"\n", values...) +} diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index ebb1449..e7e727e 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -18,6 +18,7 @@ func main() { 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) app := cli.NewApp() app.Name = "sv" @@ -66,6 +67,17 @@ func main() { Usage: "generate tag with version based on git commit messages", Action: tagHandler(git, semverProcessor), }, + { + Name: "validate-commit-message", + Aliases: []string{"vcm"}, + Usage: "use as prepare-commit-message hook to validate message", + Action: validateCommitMessageHandler(git, validateMessageProcessor), + 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"}, + &cli.StringFlag{Name: "source", Required: true, Usage: "source of the commit message"}, + }, + }, } apperr := app.Run(os.Args) diff --git a/sv/git.go b/sv/git.go index fd5193a..b649f35 100644 --- a/sv/git.go +++ b/sv/git.go @@ -28,6 +28,7 @@ type Git interface { Log(initialTag, endTag string) ([]GitCommitLog, error) Tag(version semver.Version) error Tags() ([]GitTag, error) + Branch() string } // GitCommitLog description of a single commit log @@ -115,6 +116,16 @@ func (g GitImpl) Tags() ([]GitTag, error) { return parseTagsOutput(string(out)) } +// Branch get git branch +func (GitImpl) Branch() string { + cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD") + out, err := cmd.CombinedOutput() + if err != nil { + return "" + } + return strings.TrimSpace(strings.Trim(string(out), "\n")) +} + func parseTagsOutput(input string) ([]GitTag, error) { scanner := bufio.NewScanner(strings.NewReader(input)) var result []GitTag diff --git a/sv/validatemessage.go b/sv/validatemessage.go new file mode 100644 index 0000000..1ace2f4 --- /dev/null +++ b/sv/validatemessage.go @@ -0,0 +1,64 @@ +package sv + +import ( + "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) *ValidateMessageProcessorImpl { + return &ValidateMessageProcessorImpl{ + skipBranches: skipBranches, + supportedTypes: supportedTypes, + } +} + +// ValidateMessageProcessorImpl process validate message hook. +type ValidateMessageProcessorImpl struct { + skipBranches []string + supportedTypes []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) { + //TODO add issue id (branch format on varenv) + return "", nil +} + +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] +} diff --git a/sv/validatemessage_test.go b/sv/validatemessage_test.go new file mode 100644 index 0000000..2e34160 --- /dev/null +++ b/sv/validatemessage_test.go @@ -0,0 +1,60 @@ +package sv + +import ( + "testing" +) + +func TestValidateMessageProcessorImpl_Validate(t *testing.T) { + p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}) + + tests := []struct { + name string + 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}, + + {"multi line valid message", `feat: add something + + team: x`, false}, + + {"multi line invalid message", `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}, + } + 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) + } + }) + } +} + +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) + } + }) + } +} From de703f91ceb3baf47202d72c872cdf96ebf1d1ed Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Thu, 27 Aug 2020 23:04:33 -0300 Subject: [PATCH 5/7] docs: add validate commit message on readme --- README.md | 61 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e820a02..f743c81 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,18 @@ 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| +| 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 | ### Running @@ -55,15 +57,16 @@ 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 since last version as jsons | :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: | -| help, h | Shows a list of commands or help for one command | :x: | +| 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 since last version as jsons | :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: | +| validate-commit-message, vcm | use as prepare-commit-message hook to validate message | :heavy_check_mark: | +| help, h | Shows a list of commands or help for one command | :x: | ## Development @@ -77,17 +80,17 @@ make #### Make configs -| Variable | description| -| --------- | ----------| -| BUILDOS | build OS | -| BUILDARCH | build arch | -| ECHOFLAGS | flags used on echo | -| BUILDENVS | var envs used on build | -| BUILDFLAGS | flags used on build | +| Variable | description | +| ---------- | ---------------------- | +| BUILDOS | build OS | +| BUILDARCH | build arch | +| ECHOFLAGS | flags used on echo | +| BUILDENVS | var envs used on build | +| BUILDFLAGS | flags used on build | -| Parameters | description| -| --------- | ----------| -| args | parameters that will be used on run | +| Parameters | description | +| ---------- | ----------------------------------- | +| args | parameters that will be used on run | ```bash #variables From 03a41a8d48d42b3fae31265c546e4a8504f447ce Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Thu, 27 Aug 2020 23:12:22 -0300 Subject: [PATCH 6/7] docs: fix validate commit message description --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f743c81..58bc1f7 100644 --- a/README.md +++ b/README.md @@ -57,16 +57,16 @@ 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 since last version as jsons | :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: | -| validate-commit-message, vcm | use as prepare-commit-message hook to validate message | :heavy_check_mark: | -| help, h | Shows a list of commands or help for one command | :x: | +| 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 since last version as jsons | :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: | +| 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: | ## Development From c3230811324732026dc357e1b883063726ba785a Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Mon, 31 Aug 2020 22:28:54 -0300 Subject: [PATCH 7/7] feat: add issue id to footer if defined on branch name --- README.md | 46 ++++++++++---- cmd/git-sv/config.go | 2 + cmd/git-sv/handlers.go | 3 + cmd/git-sv/main.go | 2 +- sv/validatemessage.go | 58 +++++++++++++++--- sv/validatemessage_test.go | 121 ++++++++++++++++++++++++++++++++++++- 6 files changed, 211 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 58bc1f7..b57ef85 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,20 @@ 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 | +| 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]+)(-.*)? | ### Running @@ -68,6 +70,26 @@ git-sv rn -h | 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 validate-commit-message as prepare-commit-msg hook + +Configure your .git/hooks/prepare-commit-msg + +```bash +#!/bin/sh + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +git sv vcm --path "$(pwd)" --file $COMMIT_MSG_FILE --source $COMMIT_SOURCE +``` + +tip: you can configure a directory as your global git templates using the command below, check [git config docs](https://git-scm.com/docs/git-config#Documentation/git-config.txt-inittemplateDir) for more information! + +```bash +git config --global init.templatedir '' +``` + ## Development ### Makefile diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index f18f151..f7c9dae 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -18,6 +18,8 @@ type Config struct { 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"` + BranchIssueRegex string `envconfig:"BRANCH_ISSUE_REGEX" default:"^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"` } func loadConfig() Config { diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index 7c53242..25eb19a 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -255,6 +255,9 @@ func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.Valida warn("could not enhance commit message, %s", err.Error()) return nil } + if msg == "" { + return nil + } if err := appendOnFile(msg, filepath); err != nil { return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error()) diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index e7e727e..fd5b896 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -18,7 +18,7 @@ func main() { 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) + validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex) app := cli.NewApp() app.Name = "sv" diff --git a/sv/validatemessage.go b/sv/validatemessage.go index 1ace2f4..5b0e60e 100644 --- a/sv/validatemessage.go +++ b/sv/validatemessage.go @@ -1,6 +1,7 @@ package sv import ( + "bufio" "fmt" "regexp" "strings" @@ -14,17 +15,21 @@ type ValidateMessageProcessor interface { } // NewValidateMessageProcessor ValidateMessageProcessorImpl constructor -func NewValidateMessageProcessor(skipBranches, supportedTypes []string) *ValidateMessageProcessorImpl { +func NewValidateMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex string) *ValidateMessageProcessorImpl { return &ValidateMessageProcessorImpl{ - skipBranches: skipBranches, - supportedTypes: supportedTypes, + skipBranches: skipBranches, + supportedTypes: supportedTypes, + issueKeyName: issueKeyName, + branchIssueRegex: branchIssueRegex, } } // ValidateMessageProcessorImpl process validate message hook. type ValidateMessageProcessorImpl struct { - skipBranches []string - supportedTypes []string + skipBranches []string + supportedTypes []string + issueKeyName string + branchIssueRegex string } // SkipBranch check if branch should be ignored. @@ -46,8 +51,47 @@ func (p ValidateMessageProcessorImpl) Validate(message string) error { // Enhance add metadata on commit message. func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (string, error) { - //TODO add issue id (branch format on varenv) - return "", nil + 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 { diff --git a/sv/validatemessage_test.go b/sv/validatemessage_test.go index 2e34160..c852e87 100644 --- a/sv/validatemessage_test.go +++ b/sv/validatemessage_test.go @@ -4,8 +4,46 @@ import ( "testing" ) +var issueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?" + +// messages samples start +var fullMessage = `fix: correct minor typos in code + +see the issue for details + +on typos fixed. + +Reviewed-by: Z +Refs #133` +var fullMessageWithJira = `fix: correct minor typos in code + +see the issue for details + +on typos fixed. + +Reviewed-by: Z +Refs #133 +jira: JIRA-456` +var fullMessageRefs = `fix: correct minor typos in code + +see the issue for details + +on typos fixed. + +Refs #133` +var subjectAndBodyMessage = `fix: correct minor typos in code + +see the issue for details + +on typos fixed.` +var subjectAndFooterMessage = `refactor!: drop support for Node 6 + +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"}) + p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex) tests := []struct { name string @@ -37,6 +75,39 @@ func TestValidateMessageProcessorImpl_Validate(t *testing.T) { } } +func TestValidateMessageProcessorImpl_Enhance(t *testing.T) { + p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex) + + tests := []struct { + name string + branch string + message string + want string + wantErr bool + }{ + {"issue on branch name", "JIRA-123", "fix: fix something", "\njira: JIRA-123", false}, + {"issue on branch name with description", "JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false}, + {"issue on branch name with prefix", "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false}, + {"with footer", "JIRA-123", fullMessage, "jira: JIRA-123", false}, + {"with issue on footer", "JIRA-123", fullMessageWithJira, "", false}, + {"issue on branch name with prefix and description", "feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false}, + {"no issue on branch name", "branch", "fix: fix something", "", true}, + {"unexpected branch name", "feature /JIRA-123", "fix: fix something", "", true}, + } + for _, tt := range tests { + 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) + return + } + if got != tt.want { + t.Errorf("ValidateMessageProcessorImpl.Enhance() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_firstLine(t *testing.T) { tests := []struct { name string @@ -58,3 +129,51 @@ func Test_firstLine(t *testing.T) { }) } } + +func Test_hasIssueID(t *testing.T) { + tests := []struct { + name string + message string + issueKeyName string + want bool + }{ + {"single line without issue", "feat: something", "jira", false}, + {"multi line without issue", `feat: something + +yay`, "jira", false}, + {"multi line without jira issue", `feat: something + +jira1: JIRA-123`, "jira", false}, + {"multi line with issue", `feat: something + +jira: JIRA-123`, "jira", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasIssueID(tt.message, tt.issueKeyName); got != tt.want { + t.Errorf("hasIssueID() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_hasFooter(t *testing.T) { + tests := []struct { + name string + message string + want bool + }{ + {"simple message", "feat: add something", false}, + {"full messsage", fullMessage, true}, + {"full messsage with refs", fullMessageRefs, true}, + {"subject and footer message", subjectAndFooterMessage, true}, + {"subject and body message", subjectAndBodyMessage, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasFooter(tt.message); got != tt.want { + t.Errorf("hasFooter() = %v, want %v", got, tt.want) + } + }) + } +}