mirror of
https://github.com/vbatts/git-validation.git
synced 2024-11-26 01:55:40 +00:00
Merge pull request #31 from vbatts/valued_rules
add a message_regexp rule
This commit is contained in:
commit
90191a015b
8 changed files with 205 additions and 22 deletions
|
@ -30,7 +30,9 @@ Usage of git-validation:
|
||||||
The entire default rule set is run by default:
|
The entire default rule set is run by default:
|
||||||
```console
|
```console
|
||||||
vbatts@valse ~/src/vb/git-validation (master) $ git-validation -list-rules
|
vbatts@valse ~/src/vb/git-validation (master) $ git-validation -list-rules
|
||||||
|
"dangling-whitespace" -- checking the presence of dangling whitespaces on line endings
|
||||||
"DCO" -- makes sure the commits are signed
|
"DCO" -- makes sure the commits are signed
|
||||||
|
"message_regexp" -- checks the commit message for a user provided regular expression
|
||||||
"short-subject" -- commit subjects are strictly less than 90 (github ellipsis length)
|
"short-subject" -- commit subjects are strictly less than 90 (github ellipsis length)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -92,6 +94,7 @@ vbatts@valse ~/src/vb/git-validation (master) $ GIT_CHECK_EXCLUDE="./vendor" git
|
||||||
```
|
```
|
||||||
using the `GIT_CHECK_EXCLUDE` environment variable
|
using the `GIT_CHECK_EXCLUDE` environment variable
|
||||||
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
Default rules are added by registering them to the `validate` package.
|
Default rules are added by registering them to the `validate` package.
|
||||||
|
|
17
main.go
17
main.go
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
_ "github.com/vbatts/git-validation/rules/danglingwhitespace"
|
_ "github.com/vbatts/git-validation/rules/danglingwhitespace"
|
||||||
_ "github.com/vbatts/git-validation/rules/dco"
|
_ "github.com/vbatts/git-validation/rules/dco"
|
||||||
|
_ "github.com/vbatts/git-validation/rules/messageregexp"
|
||||||
_ "github.com/vbatts/git-validation/rules/shortsubject"
|
_ "github.com/vbatts/git-validation/rules/shortsubject"
|
||||||
"github.com/vbatts/git-validation/validate"
|
"github.com/vbatts/git-validation/validate"
|
||||||
)
|
)
|
||||||
|
@ -47,10 +48,20 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// reduce the set being run
|
// rules to be used
|
||||||
rules := validate.RegisteredRules
|
var rules []validate.Rule
|
||||||
|
for _, r := range validate.RegisteredRules {
|
||||||
|
// only those that are Default
|
||||||
|
if r.Default {
|
||||||
|
rules = append(rules, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// or reduce the set being run to what the user provided
|
||||||
if *flRun != "" {
|
if *flRun != "" {
|
||||||
rules = validate.FilterRules(rules, validate.SanitizeFilters(*flRun))
|
rules = validate.FilterRules(validate.RegisteredRules, validate.SanitizeFilters(*flRun))
|
||||||
|
}
|
||||||
|
if os.Getenv("DEBUG") != "" {
|
||||||
|
log.Printf("%#v", rules) // XXX maybe reduce this list
|
||||||
}
|
}
|
||||||
|
|
||||||
var commitRange = *flCommitRange
|
var commitRange = *flCommitRange
|
||||||
|
|
|
@ -12,6 +12,7 @@ var (
|
||||||
Name: "dangling-whitespace",
|
Name: "dangling-whitespace",
|
||||||
Description: "checking the presence of dangling whitespaces on line endings",
|
Description: "checking the presence of dangling whitespaces on line endings",
|
||||||
Run: ValidateDanglingWhitespace,
|
Run: ValidateDanglingWhitespace,
|
||||||
|
Default: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateDanglingWhitespace runs Git's check to look for whitespace errors.
|
// ValidateDanglingWhitespace runs Git's check to look for whitespace errors.
|
||||||
func ValidateDanglingWhitespace(c git.CommitEntry) (vr validate.Result) {
|
func ValidateDanglingWhitespace(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
|
||||||
vr.CommitEntry = c
|
vr.CommitEntry = c
|
||||||
vr.Msg = "commit does not have any whitespace errors"
|
vr.Msg = "commit does not have any whitespace errors"
|
||||||
vr.Pass = true
|
vr.Pass = true
|
||||||
|
|
|
@ -20,11 +20,12 @@ var (
|
||||||
Name: "DCO",
|
Name: "DCO",
|
||||||
Description: "makes sure the commits are signed",
|
Description: "makes sure the commits are signed",
|
||||||
Run: ValidateDCO,
|
Run: ValidateDCO,
|
||||||
|
Default: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateDCO checks that the commit has been signed off, per the DCO process
|
// ValidateDCO checks that the commit has been signed off, per the DCO process
|
||||||
func ValidateDCO(c git.CommitEntry) (vr validate.Result) {
|
func ValidateDCO(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
|
||||||
vr.CommitEntry = c
|
vr.CommitEntry = c
|
||||||
if len(strings.Split(c["parent"], " ")) > 1 {
|
if len(strings.Split(c["parent"], " ")) > 1 {
|
||||||
vr.Pass = true
|
vr.Pass = true
|
||||||
|
|
61
rules/messageregexp/rule.go
Normal file
61
rules/messageregexp/rule.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package messageregexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vbatts/git-validation/git"
|
||||||
|
"github.com/vbatts/git-validation/validate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
validate.RegisterRule(RegexpRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// RegexpRule for validating a user provided regex on the commit messages
|
||||||
|
RegexpRule = validate.Rule{
|
||||||
|
Name: "message_regexp",
|
||||||
|
Description: "checks the commit message for a user provided regular expression",
|
||||||
|
Run: ValidateMessageRegexp,
|
||||||
|
Default: false, // only for users specifically calling it through -run ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateMessageRegexp is the message regex func to run
|
||||||
|
func ValidateMessageRegexp(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
|
||||||
|
if r.Value == "" {
|
||||||
|
vr.Pass = true
|
||||||
|
vr.Msg = "noop: message_regexp value is blank"
|
||||||
|
return vr
|
||||||
|
}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(r.Value)
|
||||||
|
vr.CommitEntry = c
|
||||||
|
if len(strings.Split(c["parent"], " ")) > 1 {
|
||||||
|
vr.Pass = true
|
||||||
|
vr.Msg = "merge commits are not checked for message_regexp"
|
||||||
|
return vr
|
||||||
|
}
|
||||||
|
|
||||||
|
hasValid := false
|
||||||
|
for _, line := range strings.Split(c["subject"], "\n") {
|
||||||
|
if re.MatchString(line) {
|
||||||
|
hasValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(c["body"], "\n") {
|
||||||
|
if re.MatchString(line) {
|
||||||
|
hasValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasValid {
|
||||||
|
vr.Pass = false
|
||||||
|
vr.Msg = fmt.Sprintf("commit message does not match %q", r.Value)
|
||||||
|
} else {
|
||||||
|
vr.Pass = true
|
||||||
|
vr.Msg = fmt.Sprintf("commit message matches %q", r.Value)
|
||||||
|
}
|
||||||
|
return vr
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ var (
|
||||||
Name: "short-subject",
|
Name: "short-subject",
|
||||||
Description: "commit subjects are strictly less than 90 (github ellipsis length)",
|
Description: "commit subjects are strictly less than 90 (github ellipsis length)",
|
||||||
Run: ValidateShortSubject,
|
Run: ValidateShortSubject,
|
||||||
|
Default: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ func init() {
|
||||||
|
|
||||||
// ValidateShortSubject checks that the commit's subject is strictly less than
|
// ValidateShortSubject checks that the commit's subject is strictly less than
|
||||||
// 90 characters (preferably not more than 72 chars).
|
// 90 characters (preferably not more than 72 chars).
|
||||||
func ValidateShortSubject(c git.CommitEntry) (vr validate.Result) {
|
func ValidateShortSubject(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
|
||||||
if len(c["subject"]) >= 90 {
|
if len(c["subject"]) >= 90 {
|
||||||
vr.Pass = false
|
vr.Pass = false
|
||||||
vr.Msg = "commit subject exceeds 90 characters"
|
vr.Msg = "commit subject exceeds 90 characters"
|
||||||
|
|
|
@ -1,33 +1,40 @@
|
||||||
package validate
|
package validate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/vbatts/git-validation/git"
|
"github.com/vbatts/git-validation/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// RegisteredRules are the standard validation to perform on git commits
|
// RegisteredRules are the avaible validation to perform on git commits
|
||||||
RegisteredRules = []Rule{}
|
RegisteredRules = []Rule{}
|
||||||
|
registerRuleLock = sync.Mutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterRule includes the Rule in the avaible set to use
|
// RegisterRule includes the Rule in the avaible set to use
|
||||||
func RegisterRule(vr Rule) {
|
func RegisterRule(vr Rule) {
|
||||||
|
registerRuleLock.Lock()
|
||||||
|
defer registerRuleLock.Unlock()
|
||||||
RegisteredRules = append(RegisteredRules, vr)
|
RegisteredRules = append(RegisteredRules, vr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule will operate over a provided git.CommitEntry, and return a result.
|
// Rule will operate over a provided git.CommitEntry, and return a result.
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
Name string // short name for reference in in the `-run=...` flag
|
Name string // short name for reference in in the `-run=...` flag
|
||||||
|
Value string // value to configure for the rule (i.e. a regexp to check for in the commit message)
|
||||||
Description string // longer Description for readability
|
Description string // longer Description for readability
|
||||||
Run func(git.CommitEntry) Result
|
Run func(Rule, git.CommitEntry) Result
|
||||||
|
Default bool // whether the registered rule is run by default
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit processes the given rules on the provided commit, and returns the result set.
|
// Commit processes the given rules on the provided commit, and returns the result set.
|
||||||
func Commit(c git.CommitEntry, rules []Rule) Results {
|
func Commit(c git.CommitEntry, rules []Rule) Results {
|
||||||
results := Results{}
|
results := Results{}
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
results = append(results, r.Run(c))
|
results = append(results, r.Run(r, c))
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
@ -54,28 +61,74 @@ func (vr Results) PassFail() (pass int, fail int) {
|
||||||
return pass, fail
|
return pass, fail
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeFilters takes a comma delimited list and returns the cleaned items in the list
|
// SanitizeFilters takes a comma delimited list and returns the trimmend and
|
||||||
func SanitizeFilters(filt string) (excludes []string) {
|
// split (on ",") items in the list
|
||||||
|
func SanitizeFilters(filtStr string) (filters []string) {
|
||||||
for _, item := range strings.Split(filt, ",") {
|
for _, item := range strings.Split(filtStr, ",") {
|
||||||
excludes = append(excludes, strings.TrimSpace(item))
|
filters = append(filters, strings.TrimSpace(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterRules takes a set of rules and a list of short names to exclude, and returns the reduced set.
|
// FilterRules takes a set of rules and a list of short names to include, and
|
||||||
// The comparison is case insensitive.
|
// returns the reduced set. The comparison is case insensitive.
|
||||||
func FilterRules(rules []Rule, excludes []string) []Rule {
|
//
|
||||||
|
// Some `includes` rules have values assigned to them.
|
||||||
|
// i.e. -run "dco,message_regexp='^JIRA-[0-9]+ [A-Z].*$'"
|
||||||
|
//
|
||||||
|
func FilterRules(rules []Rule, includes []string) []Rule {
|
||||||
ret := []Rule{}
|
ret := []Rule{}
|
||||||
|
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
for _, e := range excludes {
|
for i := range includes {
|
||||||
if strings.ToLower(r.Name) == strings.ToLower(e) {
|
if strings.Contains(includes[i], "=") {
|
||||||
|
chunks := strings.SplitN(includes[i], "=", 2)
|
||||||
|
if strings.ToLower(r.Name) == strings.ToLower(chunks[0]) {
|
||||||
|
// for these rules, the Name won't be unique per se. There may be
|
||||||
|
// multiple "regexp=" with different values. We'll need to set the
|
||||||
|
// .Value = chunk[1] and ensure r is dup'ed so they don't clobber
|
||||||
|
// each other.
|
||||||
|
newR := Rule(r)
|
||||||
|
newR.Value = chunks[1]
|
||||||
|
ret = append(ret, newR)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strings.ToLower(r.Name) == strings.ToLower(includes[i]) {
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringsSliceEqual compares two string arrays for equality
|
||||||
|
func StringsSliceEqual(a, b []string) bool {
|
||||||
|
if !sort.StringsAreSorted(a) {
|
||||||
|
sort.Strings(a)
|
||||||
|
}
|
||||||
|
if !sort.StringsAreSorted(b) {
|
||||||
|
sort.Strings(b)
|
||||||
|
}
|
||||||
|
for i := range b {
|
||||||
|
if !StringsSliceContains(a, b[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if !StringsSliceContains(b, a[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringsSliceContains checks for the presence of a word in string array
|
||||||
|
func StringsSliceContains(a []string, b string) bool {
|
||||||
|
if !sort.StringsAreSorted(a) {
|
||||||
|
sort.Strings(a)
|
||||||
|
}
|
||||||
|
i := sort.SearchStrings(a, b)
|
||||||
|
return i < len(a) && a[i] == b
|
||||||
|
}
|
||||||
|
|
52
validate/rules_test.go
Normal file
52
validate/rules_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSanitizeRules(t *testing.T) {
|
||||||
|
set := []struct {
|
||||||
|
input string
|
||||||
|
output []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "apples, oranges , bananas",
|
||||||
|
output: []string{"apples", "oranges", "bananas"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "apples, oranges , bananas, peaches='with cream'",
|
||||||
|
output: []string{"apples", "oranges", "bananas", "peaches='with cream'"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range set {
|
||||||
|
filt := SanitizeFilters(set[i].input)
|
||||||
|
if !StringsSliceEqual(filt, set[i].output) {
|
||||||
|
t.Errorf("expected output like %v, but got %v", set[i].output, filt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSliceHelpers(t *testing.T) {
|
||||||
|
set := []struct {
|
||||||
|
A, B []string
|
||||||
|
Equal bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
A: []string{"apples", "bananas", "oranges", "mango"},
|
||||||
|
B: []string{"oranges", "bananas", "apples", "mango"},
|
||||||
|
Equal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: []string{"apples", "bananas", "oranges", "mango"},
|
||||||
|
B: []string{"waffles"},
|
||||||
|
Equal: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := range set {
|
||||||
|
got := StringsSliceEqual(set[i].A, set[i].B)
|
||||||
|
if got != set[i].Equal {
|
||||||
|
t.Errorf("expected %d A and B comparison to be %t, but got %t", i, set[i].Equal, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue