mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-22 08:20:39 +00:00
Merge pull request #5 from bvieira/prepare-commit
Validate commit message action
This commit is contained in:
commit
05135364f9
9
Makefile
9
Makefile
@ -50,9 +50,10 @@ release:
|
|||||||
make build
|
make build
|
||||||
@zip -j bin/git-sv_$(VERSION)_$(BUILDOS)_$(BUILDARCH).zip bin/$(BUILDOS)_$(BUILDARCH)/$(BIN)
|
@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:
|
release-all:
|
||||||
@rm -rf bin
|
@rm -rf bin
|
||||||
BUILDOS=linux make release
|
|
||||||
BUILDOS=darwin make release
|
VERSION=$(shell git sv nv) BUILDOS=linux make release
|
||||||
BUILDOS=windows make release
|
VERSION=$(shell git sv nv) BUILDOS=darwin make release
|
||||||
|
VERSION=$(shell git sv nv) BUILDOS=windows make release
|
||||||
|
83
README.md
83
README.md
@ -12,16 +12,20 @@ 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||
|
| MAJOR_VERSION_TYPES | types used to bump major version | |
|
||||||
|MINOR_VERSION_TYPES|types used to bump minor version|feat|
|
| 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|
|
| 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|
|
| 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:|
|
| 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:|
|
| 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|
|
| TAG_PATTERN | tag version pattern | %d.%d.%d |
|
||||||
|RELEASE_NOTES_TAGS|release notes headers for each visible type|fix:Bug Fixes,feat:Features|
|
| 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
|
### Running
|
||||||
|
|
||||||
@ -55,15 +59,36 @@ git-sv rn -h
|
|||||||
|
|
||||||
##### Available commands
|
##### Available commands
|
||||||
|
|
||||||
| Variable | description | has options |
|
| Variable | description | has options |
|
||||||
| --------- | ---------- | :----------: |
|
| ---------------------------- | ------------------------------------------------------------- | :----------------: |
|
||||||
| current-version, cv | get last released version from git | :x: |
|
| current-version, cv | get last released version from git | :x: |
|
||||||
| next-version, nv | generate the next version based on git commit messages | :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: |
|
| commit-log, cl | list all commit logs since last version as jsons | :heavy_check_mark: |
|
||||||
| 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: |
|
||||||
| help, h | Shows a list of commands or help for one command | :x: |
|
| validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: |
|
||||||
|
| help, h | Shows a list of commands or help for one command | :x: |
|
||||||
|
|
||||||
|
##### Use 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 '<YOUR TEMPLATE DIR>'
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@ -77,17 +102,17 @@ make
|
|||||||
|
|
||||||
#### Make configs
|
#### Make configs
|
||||||
|
|
||||||
| Variable | description|
|
| Variable | description |
|
||||||
| --------- | ----------|
|
| ---------- | ---------------------- |
|
||||||
| BUILDOS | build OS |
|
| BUILDOS | build OS |
|
||||||
| BUILDARCH | build arch |
|
| BUILDARCH | build arch |
|
||||||
| ECHOFLAGS | flags used on echo |
|
| ECHOFLAGS | flags used on echo |
|
||||||
| BUILDENVS | var envs used on build |
|
| BUILDENVS | var envs used on build |
|
||||||
| BUILDFLAGS | flags used on build |
|
| BUILDFLAGS | flags used on build |
|
||||||
|
|
||||||
| Parameters | description|
|
| Parameters | description |
|
||||||
| --------- | ----------|
|
| ---------- | ----------------------------------- |
|
||||||
| args | parameters that will be used on run |
|
| args | parameters that will be used on run |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#variables
|
#variables
|
||||||
|
@ -8,14 +8,18 @@ import (
|
|||||||
|
|
||||||
// Config env vars for cli configuration
|
// Config env vars for cli configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
|
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
|
||||||
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
|
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,docs,fix,perf,refactor,style,test"`
|
||||||
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
|
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
|
||||||
BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE:,BREAKING CHANGES:"`
|
BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE:,BREAKING CHANGES:"`
|
||||||
IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira:,JIRA:,Jira:"`
|
IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira:,JIRA:,Jira:"`
|
||||||
TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
|
TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
|
||||||
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
|
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 {
|
func loadConfig() Config {
|
||||||
|
@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"sv4git/sv"
|
"sv4git/sv"
|
||||||
"time"
|
"time"
|
||||||
@ -228,3 +230,58 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
|
|||||||
return nil
|
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 msg == "" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
7
cmd/git-sv/log.go
Normal file
7
cmd/git-sv/log.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func warn(format string, values ...interface{}) {
|
||||||
|
fmt.Printf("WARN: "+format+"\n", values...)
|
||||||
|
}
|
@ -18,6 +18,7 @@ func main() {
|
|||||||
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)
|
||||||
|
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "sv"
|
app.Name = "sv"
|
||||||
@ -66,6 +67,17 @@ 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: "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)
|
apperr := app.Run(os.Args)
|
||||||
|
2
go.mod
2
go.mod
@ -6,5 +6,5 @@ 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.1.1
|
github.com/urfave/cli/v2 v2.2.0
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
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/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.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
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/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
11
sv/git.go
11
sv/git.go
@ -28,6 +28,7 @@ type Git interface {
|
|||||||
Log(initialTag, endTag string) ([]GitCommitLog, error)
|
Log(initialTag, endTag string) ([]GitCommitLog, error)
|
||||||
Tag(version semver.Version) error
|
Tag(version semver.Version) error
|
||||||
Tags() ([]GitTag, error)
|
Tags() ([]GitTag, error)
|
||||||
|
Branch() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitCommitLog description of a single commit log
|
// GitCommitLog description of a single commit log
|
||||||
@ -115,6 +116,16 @@ func (g GitImpl) Tags() ([]GitTag, error) {
|
|||||||
return parseTagsOutput(string(out))
|
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) {
|
func parseTagsOutput(input string) ([]GitTag, error) {
|
||||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||||
var result []GitTag
|
var result []GitTag
|
||||||
|
@ -13,8 +13,8 @@ func Test_parseTagsOutput(t *testing.T) {
|
|||||||
want []GitTag
|
want []GitTag
|
||||||
wantErr bool
|
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},
|
{"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{GitTag{Name: "1.0.0", Date: time.Time{}}}, false},
|
{"without date", "#1.0.0", []GitTag{{Name: "1.0.0", Date: time.Time{}}}, false},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
108
sv/validatemessage.go
Normal file
108
sv/validatemessage.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
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]
|
||||||
|
}
|
179
sv/validatemessage_test.go
Normal file
179
sv/validatemessage_test.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package sv
|
||||||
|
|
||||||
|
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"}, "jira", issueRegex)
|
||||||
|
|
||||||
|
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 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
|
||||||
|
value string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"empty string", "", ""},
|
||||||
|
|
||||||
|
{"single line string", "single line", "single line"},
|
||||||
|
|
||||||
|
{"multi line string", `first line
|
||||||
|
last line`, "first line"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := firstLine(tt.value); got != tt.want {
|
||||||
|
t.Errorf("firstLine() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_hasIssueID(t *testing.T) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user