Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand Down Expand Up @@ -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.
Expand Down
17 changes: 14 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion rules/danglingwhitespace/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var (
Name: "dangling-whitespace",
Description: "checking the presence of dangling whitespaces on line endings",
Run: ValidateDanglingWhitespace,
Default: true,
}
)

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion rules/dco/dco.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions rules/messageregexp/rule.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion rules/shortsubject/shortsubject.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var (
Name: "short-subject",
Description: "commit subjects are strictly less than 90 (github ellipsis length)",
Run: ValidateShortSubject,
Default: true,
}
)

Expand All @@ -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"
Expand Down
85 changes: 69 additions & 16 deletions validate/rules.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
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 are the avaible validation to perform on git commits
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
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
}
Expand All @@ -54,28 +61,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
}
52 changes: 52 additions & 0 deletions validate/rules_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}