Skip to content

Commit 90191a0

Browse files
authored
Merge pull request #31 from vbatts/valued_rules
add a message_regexp rule
2 parents cc3ab48 + 00823a3 commit 90191a0

File tree

8 files changed

+205
-22
lines changed

8 files changed

+205
-22
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ Usage of git-validation:
3030
The entire default rule set is run by default:
3131
```console
3232
vbatts@valse ~/src/vb/git-validation (master) $ git-validation -list-rules
33+
"dangling-whitespace" -- checking the presence of dangling whitespaces on line endings
3334
"DCO" -- makes sure the commits are signed
35+
"message_regexp" -- checks the commit message for a user provided regular expression
3436
"short-subject" -- commit subjects are strictly less than 90 (github ellipsis length)
3537
```
3638

@@ -92,6 +94,7 @@ vbatts@valse ~/src/vb/git-validation (master) $ GIT_CHECK_EXCLUDE="./vendor" git
9294
```
9395
using the `GIT_CHECK_EXCLUDE` environment variable
9496

97+
9598
## Rules
9699

97100
Default rules are added by registering them to the `validate` package.

main.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
_ "github.com/vbatts/git-validation/rules/danglingwhitespace"
1111
_ "github.com/vbatts/git-validation/rules/dco"
12+
_ "github.com/vbatts/git-validation/rules/messageregexp"
1213
_ "github.com/vbatts/git-validation/rules/shortsubject"
1314
"github.com/vbatts/git-validation/validate"
1415
)
@@ -47,10 +48,20 @@ func main() {
4748
return
4849
}
4950

50-
// reduce the set being run
51-
rules := validate.RegisteredRules
51+
// rules to be used
52+
var rules []validate.Rule
53+
for _, r := range validate.RegisteredRules {
54+
// only those that are Default
55+
if r.Default {
56+
rules = append(rules, r)
57+
}
58+
}
59+
// or reduce the set being run to what the user provided
5260
if *flRun != "" {
53-
rules = validate.FilterRules(rules, validate.SanitizeFilters(*flRun))
61+
rules = validate.FilterRules(validate.RegisteredRules, validate.SanitizeFilters(*flRun))
62+
}
63+
if os.Getenv("DEBUG") != "" {
64+
log.Printf("%#v", rules) // XXX maybe reduce this list
5465
}
5566

5667
var commitRange = *flCommitRange

rules/danglingwhitespace/rule.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var (
1212
Name: "dangling-whitespace",
1313
Description: "checking the presence of dangling whitespaces on line endings",
1414
Run: ValidateDanglingWhitespace,
15+
Default: true,
1516
}
1617
)
1718

@@ -20,7 +21,7 @@ func init() {
2021
}
2122

2223
// ValidateDanglingWhitespace runs Git's check to look for whitespace errors.
23-
func ValidateDanglingWhitespace(c git.CommitEntry) (vr validate.Result) {
24+
func ValidateDanglingWhitespace(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
2425
vr.CommitEntry = c
2526
vr.Msg = "commit does not have any whitespace errors"
2627
vr.Pass = true

rules/dco/dco.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ var (
2020
Name: "DCO",
2121
Description: "makes sure the commits are signed",
2222
Run: ValidateDCO,
23+
Default: true,
2324
}
2425
)
2526

2627
// ValidateDCO checks that the commit has been signed off, per the DCO process
27-
func ValidateDCO(c git.CommitEntry) (vr validate.Result) {
28+
func ValidateDCO(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
2829
vr.CommitEntry = c
2930
if len(strings.Split(c["parent"], " ")) > 1 {
3031
vr.Pass = true

rules/messageregexp/rule.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package messageregexp
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/vbatts/git-validation/git"
9+
"github.com/vbatts/git-validation/validate"
10+
)
11+
12+
func init() {
13+
validate.RegisterRule(RegexpRule)
14+
}
15+
16+
var (
17+
// RegexpRule for validating a user provided regex on the commit messages
18+
RegexpRule = validate.Rule{
19+
Name: "message_regexp",
20+
Description: "checks the commit message for a user provided regular expression",
21+
Run: ValidateMessageRegexp,
22+
Default: false, // only for users specifically calling it through -run ...
23+
}
24+
)
25+
26+
// ValidateMessageRegexp is the message regex func to run
27+
func ValidateMessageRegexp(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
28+
if r.Value == "" {
29+
vr.Pass = true
30+
vr.Msg = "noop: message_regexp value is blank"
31+
return vr
32+
}
33+
34+
re := regexp.MustCompile(r.Value)
35+
vr.CommitEntry = c
36+
if len(strings.Split(c["parent"], " ")) > 1 {
37+
vr.Pass = true
38+
vr.Msg = "merge commits are not checked for message_regexp"
39+
return vr
40+
}
41+
42+
hasValid := false
43+
for _, line := range strings.Split(c["subject"], "\n") {
44+
if re.MatchString(line) {
45+
hasValid = true
46+
}
47+
}
48+
for _, line := range strings.Split(c["body"], "\n") {
49+
if re.MatchString(line) {
50+
hasValid = true
51+
}
52+
}
53+
if !hasValid {
54+
vr.Pass = false
55+
vr.Msg = fmt.Sprintf("commit message does not match %q", r.Value)
56+
} else {
57+
vr.Pass = true
58+
vr.Msg = fmt.Sprintf("commit message matches %q", r.Value)
59+
}
60+
return vr
61+
}

rules/shortsubject/shortsubject.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var (
1111
Name: "short-subject",
1212
Description: "commit subjects are strictly less than 90 (github ellipsis length)",
1313
Run: ValidateShortSubject,
14+
Default: true,
1415
}
1516
)
1617

@@ -20,7 +21,7 @@ func init() {
2021

2122
// ValidateShortSubject checks that the commit's subject is strictly less than
2223
// 90 characters (preferably not more than 72 chars).
23-
func ValidateShortSubject(c git.CommitEntry) (vr validate.Result) {
24+
func ValidateShortSubject(r validate.Rule, c git.CommitEntry) (vr validate.Result) {
2425
if len(c["subject"]) >= 90 {
2526
vr.Pass = false
2627
vr.Msg = "commit subject exceeds 90 characters"

validate/rules.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
11
package validate
22

33
import (
4+
"sort"
45
"strings"
6+
"sync"
57

68
"github.com/vbatts/git-validation/git"
79
)
810

911
var (
10-
// RegisteredRules are the standard validation to perform on git commits
11-
RegisteredRules = []Rule{}
12+
// RegisteredRules are the avaible validation to perform on git commits
13+
RegisteredRules = []Rule{}
14+
registerRuleLock = sync.Mutex{}
1215
)
1316

1417
// RegisterRule includes the Rule in the avaible set to use
1518
func RegisterRule(vr Rule) {
19+
registerRuleLock.Lock()
20+
defer registerRuleLock.Unlock()
1621
RegisteredRules = append(RegisteredRules, vr)
1722
}
1823

1924
// Rule will operate over a provided git.CommitEntry, and return a result.
2025
type Rule struct {
2126
Name string // short name for reference in in the `-run=...` flag
27+
Value string // value to configure for the rule (i.e. a regexp to check for in the commit message)
2228
Description string // longer Description for readability
23-
Run func(git.CommitEntry) Result
29+
Run func(Rule, git.CommitEntry) Result
30+
Default bool // whether the registered rule is run by default
2431
}
2532

2633
// Commit processes the given rules on the provided commit, and returns the result set.
2734
func Commit(c git.CommitEntry, rules []Rule) Results {
2835
results := Results{}
2936
for _, r := range rules {
30-
results = append(results, r.Run(c))
37+
results = append(results, r.Run(r, c))
3138
}
3239
return results
3340
}
@@ -54,28 +61,74 @@ func (vr Results) PassFail() (pass int, fail int) {
5461
return pass, fail
5562
}
5663

57-
// SanitizeFilters takes a comma delimited list and returns the cleaned items in the list
58-
func SanitizeFilters(filt string) (excludes []string) {
59-
60-
for _, item := range strings.Split(filt, ",") {
61-
excludes = append(excludes, strings.TrimSpace(item))
64+
// SanitizeFilters takes a comma delimited list and returns the trimmend and
65+
// split (on ",") items in the list
66+
func SanitizeFilters(filtStr string) (filters []string) {
67+
for _, item := range strings.Split(filtStr, ",") {
68+
filters = append(filters, strings.TrimSpace(item))
6269
}
63-
6470
return
6571
}
6672

67-
// FilterRules takes a set of rules and a list of short names to exclude, and returns the reduced set.
68-
// The comparison is case insensitive.
69-
func FilterRules(rules []Rule, excludes []string) []Rule {
73+
// FilterRules takes a set of rules and a list of short names to include, and
74+
// returns the reduced set. The comparison is case insensitive.
75+
//
76+
// Some `includes` rules have values assigned to them.
77+
// i.e. -run "dco,message_regexp='^JIRA-[0-9]+ [A-Z].*$'"
78+
//
79+
func FilterRules(rules []Rule, includes []string) []Rule {
7080
ret := []Rule{}
7181

7282
for _, r := range rules {
73-
for _, e := range excludes {
74-
if strings.ToLower(r.Name) == strings.ToLower(e) {
75-
ret = append(ret, r)
83+
for i := range includes {
84+
if strings.Contains(includes[i], "=") {
85+
chunks := strings.SplitN(includes[i], "=", 2)
86+
if strings.ToLower(r.Name) == strings.ToLower(chunks[0]) {
87+
// for these rules, the Name won't be unique per se. There may be
88+
// multiple "regexp=" with different values. We'll need to set the
89+
// .Value = chunk[1] and ensure r is dup'ed so they don't clobber
90+
// each other.
91+
newR := Rule(r)
92+
newR.Value = chunks[1]
93+
ret = append(ret, newR)
94+
}
95+
} else {
96+
if strings.ToLower(r.Name) == strings.ToLower(includes[i]) {
97+
ret = append(ret, r)
98+
}
7699
}
77100
}
78101
}
79102

80103
return ret
81104
}
105+
106+
// StringsSliceEqual compares two string arrays for equality
107+
func StringsSliceEqual(a, b []string) bool {
108+
if !sort.StringsAreSorted(a) {
109+
sort.Strings(a)
110+
}
111+
if !sort.StringsAreSorted(b) {
112+
sort.Strings(b)
113+
}
114+
for i := range b {
115+
if !StringsSliceContains(a, b[i]) {
116+
return false
117+
}
118+
}
119+
for i := range a {
120+
if !StringsSliceContains(b, a[i]) {
121+
return false
122+
}
123+
}
124+
return true
125+
}
126+
127+
// StringsSliceContains checks for the presence of a word in string array
128+
func StringsSliceContains(a []string, b string) bool {
129+
if !sort.StringsAreSorted(a) {
130+
sort.Strings(a)
131+
}
132+
i := sort.SearchStrings(a, b)
133+
return i < len(a) && a[i] == b
134+
}

validate/rules_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package validate
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestSanitizeRules(t *testing.T) {
8+
set := []struct {
9+
input string
10+
output []string
11+
}{
12+
{
13+
input: "apples, oranges , bananas",
14+
output: []string{"apples", "oranges", "bananas"},
15+
},
16+
{
17+
input: "apples, oranges , bananas, peaches='with cream'",
18+
output: []string{"apples", "oranges", "bananas", "peaches='with cream'"},
19+
},
20+
}
21+
22+
for i := range set {
23+
filt := SanitizeFilters(set[i].input)
24+
if !StringsSliceEqual(filt, set[i].output) {
25+
t.Errorf("expected output like %v, but got %v", set[i].output, filt)
26+
}
27+
}
28+
}
29+
30+
func TestSliceHelpers(t *testing.T) {
31+
set := []struct {
32+
A, B []string
33+
Equal bool
34+
}{
35+
{
36+
A: []string{"apples", "bananas", "oranges", "mango"},
37+
B: []string{"oranges", "bananas", "apples", "mango"},
38+
Equal: true,
39+
},
40+
{
41+
A: []string{"apples", "bananas", "oranges", "mango"},
42+
B: []string{"waffles"},
43+
Equal: false,
44+
},
45+
}
46+
for i := range set {
47+
got := StringsSliceEqual(set[i].A, set[i].B)
48+
if got != set[i].Equal {
49+
t.Errorf("expected %d A and B comparison to be %t, but got %t", i, set[i].Equal, got)
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)