From d9bf419b441e9409c6b61835c93757660f134126 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 9 Oct 2017 13:21:23 -0400 Subject: [PATCH 1/2] validate: prep for rules that have a value where needed, some rules may need to pass in a value like for a reguler expression. Signed-off-by: Vincent Batts --- validate/rules.go | 78 +++++++++++++++++++++++++++++++++++------- validate/rules_test.go | 52 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 validate/rules_test.go diff --git a/validate/rules.go b/validate/rules.go index b88e0e9..d08ecfd 100644 --- a/validate/rules.go +++ b/validate/rules.go @@ -1,24 +1,30 @@ package validate import ( + "sort" "strings" + "sync" "github.com/vbatts/git-validation/git" ) var ( // RegisteredRules are the standard validation to perform on git commits - RegisteredRules = []Rule{} + RegisteredRules = []Rule{} + registerRuleLock = sync.Mutex{} ) // RegisterRule includes the Rule in the avaible set to use func RegisterRule(vr Rule) { + registerRuleLock.Lock() + defer registerRuleLock.Unlock() RegisteredRules = append(RegisteredRules, vr) } // Rule will operate over a provided git.CommitEntry, and return a result. type Rule struct { 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 Run func(git.CommitEntry) Result } @@ -54,28 +60,74 @@ func (vr Results) PassFail() (pass int, fail int) { return pass, fail } -// SanitizeFilters takes a comma delimited list and returns the cleaned items in the list -func SanitizeFilters(filt string) (excludes []string) { - - for _, item := range strings.Split(filt, ",") { - excludes = append(excludes, strings.TrimSpace(item)) +// SanitizeFilters takes a comma delimited list and returns the trimmend and +// split (on ",") items in the list +func SanitizeFilters(filtStr string) (filters []string) { + for _, item := range strings.Split(filtStr, ",") { + filters = append(filters, strings.TrimSpace(item)) } - return } -// FilterRules takes a set of rules and a list of short names to exclude, and returns the reduced set. -// The comparison is case insensitive. -func FilterRules(rules []Rule, excludes []string) []Rule { +// FilterRules takes a set of rules and a list of short names to include, and +// returns the reduced set. The comparison is case insensitive. +// +// 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{} for _, r := range rules { - for _, e := range excludes { - if strings.ToLower(r.Name) == strings.ToLower(e) { - ret = append(ret, r) + for i := range includes { + 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) + } } } } 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 +} diff --git a/validate/rules_test.go b/validate/rules_test.go new file mode 100644 index 0000000..290973f --- /dev/null +++ b/validate/rules_test.go @@ -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 %q, but got %q", i, set[i].Equal, got) + } + } +} From 00823a324b853af2005ede5374d659869f041d21 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 9 Oct 2017 15:49:59 -0400 Subject: [PATCH 2/2] message_regexp: regular expression rule for messages Fixes: #30 now you can add a regular expression to the rules to be run i.e. `git-validation -run "dco,message_regexp='^JIRA-[0-9]+ [A-Z].*$'"` Signed-off-by: Vincent Batts --- README.md | 3 ++ main.go | 17 +++++++-- rules/danglingwhitespace/rule.go | 3 +- rules/dco/dco.go | 3 +- rules/messageregexp/rule.go | 61 ++++++++++++++++++++++++++++++ rules/shortsubject/shortsubject.go | 3 +- validate/rules.go | 7 ++-- validate/rules_test.go | 2 +- 8 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 rules/messageregexp/rule.go diff --git a/README.md b/README.md index 6ecab1d..cbeb355 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ Usage of git-validation: The entire default rule set is run by default: ```console 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 +"message_regexp" -- checks the commit message for a user provided regular expression "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 + ## Rules Default rules are added by registering them to the `validate` package. diff --git a/main.go b/main.go index 8b6876b..cd5f271 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( _ "github.com/vbatts/git-validation/rules/danglingwhitespace" _ "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/validate" ) @@ -47,10 +48,20 @@ func main() { return } - // reduce the set being run - rules := validate.RegisteredRules + // rules to be used + 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 != "" { - 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 diff --git a/rules/danglingwhitespace/rule.go b/rules/danglingwhitespace/rule.go index 14ad034..dab3a98 100644 --- a/rules/danglingwhitespace/rule.go +++ b/rules/danglingwhitespace/rule.go @@ -12,6 +12,7 @@ var ( Name: "dangling-whitespace", Description: "checking the presence of dangling whitespaces on line endings", Run: ValidateDanglingWhitespace, + Default: true, } ) @@ -20,7 +21,7 @@ func init() { } // 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.Msg = "commit does not have any whitespace errors" vr.Pass = true diff --git a/rules/dco/dco.go b/rules/dco/dco.go index 6371720..a42ea06 100644 --- a/rules/dco/dco.go +++ b/rules/dco/dco.go @@ -20,11 +20,12 @@ var ( Name: "DCO", Description: "makes sure the commits are signed", Run: ValidateDCO, + Default: true, } ) // 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 if len(strings.Split(c["parent"], " ")) > 1 { vr.Pass = true diff --git a/rules/messageregexp/rule.go b/rules/messageregexp/rule.go new file mode 100644 index 0000000..98587f2 --- /dev/null +++ b/rules/messageregexp/rule.go @@ -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 +} diff --git a/rules/shortsubject/shortsubject.go b/rules/shortsubject/shortsubject.go index f09e884..2aff131 100644 --- a/rules/shortsubject/shortsubject.go +++ b/rules/shortsubject/shortsubject.go @@ -11,6 +11,7 @@ var ( Name: "short-subject", Description: "commit subjects are strictly less than 90 (github ellipsis length)", Run: ValidateShortSubject, + Default: true, } ) @@ -20,7 +21,7 @@ func init() { // ValidateShortSubject checks that the commit's subject is strictly less than // 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 { vr.Pass = false vr.Msg = "commit subject exceeds 90 characters" diff --git a/validate/rules.go b/validate/rules.go index d08ecfd..38126a4 100644 --- a/validate/rules.go +++ b/validate/rules.go @@ -9,7 +9,7 @@ import ( ) var ( - // RegisteredRules are the standard validation to perform on git commits + // RegisteredRules are the avaible validation to perform on git commits RegisteredRules = []Rule{} registerRuleLock = sync.Mutex{} ) @@ -26,14 +26,15 @@ type Rule struct { 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 - 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. func Commit(c git.CommitEntry, rules []Rule) Results { results := Results{} for _, r := range rules { - results = append(results, r.Run(c)) + results = append(results, r.Run(r, c)) } return results } diff --git a/validate/rules_test.go b/validate/rules_test.go index 290973f..e9fe3f9 100644 --- a/validate/rules_test.go +++ b/validate/rules_test.go @@ -46,7 +46,7 @@ func TestSliceHelpers(t *testing.T) { 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 %q, but got %q", i, set[i].Equal, got) + t.Errorf("expected %d A and B comparison to be %t, but got %t", i, set[i].Equal, got) } } }