Skip to content

Commit c10ba9c

Browse files
committed
Initial commit
Signed-off-by: Vincent Batts <[email protected]>
0 parents  commit c10ba9c

File tree

7 files changed

+307
-0
lines changed

7 files changed

+307
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*~
2+
git-validation

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Vincent Batts
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# git-validation
2+
3+
A way to do per git commit validation
4+
5+

git/commits.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package git
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"os"
9+
"os/exec"
10+
"strings"
11+
)
12+
13+
// Commits returns a set of commits.
14+
// If commitrange is a git still range 12345...54321, then it will be isolated set of commits.
15+
// If commitrange is a single commit, all ancestor commits up through the hash provided.
16+
func Commits(commitrange string) ([]CommitEntry, error) {
17+
output, err := exec.Command("git", "log", prettyFormat+formatCommit, commitrange).Output()
18+
if err != nil {
19+
return nil, err
20+
}
21+
commitHashes := strings.Split(strings.TrimSpace(string(output)), "\n")
22+
commits := make([]CommitEntry, len(commitHashes))
23+
for i, commitHash := range commitHashes {
24+
c, err := LogCommit(commitHash)
25+
if err != nil {
26+
return commits, err
27+
}
28+
commits[i] = *c
29+
}
30+
return commits, nil
31+
}
32+
33+
// CommitEntry represents a single commit's information from `git`
34+
type CommitEntry map[string]string
35+
36+
var (
37+
prettyFormat = `--pretty=format:`
38+
formatSubject = `%s`
39+
formatBody = `%b`
40+
formatCommit = `%H`
41+
formatAuthorName = `%aN`
42+
formatAuthorEmail = `%aE`
43+
formatCommitterName = `%cN`
44+
formatCommitterEmail = `%cE`
45+
formatSigner = `%GS`
46+
formatCommitNotes = `%N`
47+
formatMap = `{"commit": "%H", "abbreviated_commit": "%h", "tree": "%T", "abbreviated_tree": "%t", "parent": "%P", "abbreviated_parent": "%p", "refs": "%D", "encoding": "%e", "sanitized_subject_line": "%f", "verification_flag": "%G?", "signer_key": "%GK", "author_date": "%aD" , "committer_date": "%cD" }`
48+
)
49+
50+
// LogCommit assembles the full information on a commit from its commit hash
51+
func LogCommit(commit string) (*CommitEntry, error) {
52+
buf := bytes.NewBuffer([]byte{})
53+
cmd := exec.Command("git", "log", "-1", prettyFormat+formatMap, commit)
54+
cmd.Stdout = buf
55+
cmd.Stderr = os.Stderr
56+
57+
if err := cmd.Run(); err != nil {
58+
log.Println(strings.Join(cmd.Args, " "))
59+
return nil, err
60+
}
61+
c := CommitEntry{}
62+
output := buf.Bytes()
63+
if err := json.Unmarshal(output, &c); err != nil {
64+
fmt.Println(string(output))
65+
return nil, err
66+
}
67+
68+
// any user provided fields can't be sanitized for the mock-json marshal above
69+
for k, v := range map[string]string{
70+
"subject": formatSubject,
71+
"body": formatBody,
72+
"author_name": formatAuthorName,
73+
"author_email": formatAuthorEmail,
74+
"committer_name": formatCommitterName,
75+
"committer_email": formatCommitterEmail,
76+
"commit_notes": formatCommitNotes,
77+
"signer": formatSigner,
78+
} {
79+
output, err := exec.Command("git", "log", "-1", prettyFormat+v, commit).Output()
80+
if err != nil {
81+
return nil, err
82+
}
83+
c[k] = strings.TrimSpace(string(output))
84+
}
85+
86+
return &c, nil
87+
}
88+
89+
// FetchHeadCommit returns the hash of FETCH_HEAD
90+
func FetchHeadCommit() (string, error) {
91+
output, err := exec.Command("git", "rev-parse", "--verify", "FETCH_HEAD").Output()
92+
if err != nil {
93+
return "", err
94+
}
95+
return strings.TrimSpace(string(output)), nil
96+
}
97+
98+
// HeadCommit returns the hash of HEAD
99+
func HeadCommit() (string, error) {
100+
output, err := exec.Command("git", "rev-parse", "--verify", "HEAD").Output()
101+
if err != nil {
102+
return "", err
103+
}
104+
return strings.TrimSpace(string(output)), nil
105+
}

main.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
9+
"github.com/vbatts/git-validation/git"
10+
_ "github.com/vbatts/git-validation/rules/dco"
11+
"github.com/vbatts/git-validation/validate"
12+
)
13+
14+
var (
15+
flCommitRange = flag.String("range", "", "use this commit range instead")
16+
flListRules = flag.Bool("list-rules", false, "list the rules registered")
17+
flRun = flag.String("run", "", "comma delimited list of rules to run. Defaults to all.")
18+
flVerbose = flag.Bool("v", false, "verbose")
19+
)
20+
21+
func main() {
22+
flag.Parse()
23+
24+
if *flListRules {
25+
for _, r := range validate.RegisteredRules {
26+
fmt.Printf("%q -- %s\n", r.Name, r.Description)
27+
}
28+
return
29+
}
30+
31+
var commitrange string
32+
if *flCommitRange != "" {
33+
commitrange = *flCommitRange
34+
} else {
35+
var err error
36+
commitrange, err = git.FetchHeadCommit()
37+
if err != nil {
38+
log.Fatal(err)
39+
}
40+
}
41+
42+
c, err := git.Commits(commitrange)
43+
if err != nil {
44+
log.Fatal(err)
45+
}
46+
47+
results := validate.Results{}
48+
for _, commit := range c {
49+
fmt.Printf(" * %s %s ... ", commit["abbreviated_commit"], commit["subject"])
50+
vr := validate.Commit(commit, validate.RegisteredRules)
51+
results = append(results, vr...)
52+
if _, fail := vr.PassFail(); fail == 0 {
53+
fmt.Println("PASS")
54+
if *flVerbose {
55+
for _, r := range vr {
56+
if r.Pass {
57+
fmt.Printf(" - %s\n", r.Msg)
58+
}
59+
}
60+
}
61+
} else {
62+
fmt.Println("FAIL")
63+
// default, only print out failed validations
64+
for _, r := range vr {
65+
if !r.Pass {
66+
fmt.Printf(" - %s\n", r.Msg)
67+
}
68+
}
69+
}
70+
}
71+
_, fail := results.PassFail()
72+
if fail > 0 {
73+
fmt.Printf("%d issues to fix\n", fail)
74+
os.Exit(1)
75+
}
76+
}

rules/dco/dco.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package dco
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/vbatts/git-validation/git"
8+
"github.com/vbatts/git-validation/validate"
9+
)
10+
11+
func init() {
12+
validate.RegisterRule(DcoRule)
13+
}
14+
15+
var (
16+
ValidDCO = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`)
17+
DcoRule = validate.Rule{
18+
Name: "DCO",
19+
Description: "makes sure the commits are signed",
20+
Run: ValidateDCO,
21+
}
22+
)
23+
24+
func ValidateDCO(c git.CommitEntry) (vr validate.Result) {
25+
vr.CommitEntry = c
26+
if len(strings.Split(c["parent"], " ")) > 1 {
27+
vr.Pass = true
28+
vr.Msg = "merge commits do not require DCO"
29+
return vr
30+
}
31+
32+
hasValid := false
33+
for _, line := range strings.Split(c["body"], "\n") {
34+
if ValidDCO.MatchString(line) {
35+
hasValid = true
36+
}
37+
}
38+
if !hasValid {
39+
vr.Pass = false
40+
vr.Msg = "does not have a valid DCO"
41+
} else {
42+
vr.Pass = true
43+
vr.Msg = "has a valid DCO"
44+
}
45+
46+
return vr
47+
}

validate/rules.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package validate
2+
3+
import "github.com/vbatts/git-validation/git"
4+
5+
var (
6+
// RegisteredRules are the standard validation to perform on git commits
7+
RegisteredRules = []Rule{}
8+
)
9+
10+
// RegisterRule includes the Rule in the avaible set to use
11+
func RegisterRule(vr Rule) {
12+
RegisteredRules = append(RegisteredRules, vr)
13+
}
14+
15+
// Rule will operate over a provided git.CommitEntry, and return a result.
16+
type Rule struct {
17+
Name string // short name for reference in in the `-run=...` flag
18+
Description string // longer Description for readability
19+
Run func(git.CommitEntry) Result
20+
}
21+
22+
// Commit processes the given rules on the provided commit, and returns the result set.
23+
func Commit(c git.CommitEntry, rules []Rule) Results {
24+
results := Results{}
25+
for _, r := range rules {
26+
results = append(results, r.Run(c))
27+
}
28+
return results
29+
}
30+
31+
// Result is the result for a single validation of a commit.
32+
type Result struct {
33+
CommitEntry git.CommitEntry
34+
Pass bool
35+
Msg string
36+
}
37+
38+
// Results is a set of results. This is type makes it easy for the following function.
39+
type Results []Result
40+
41+
// PassFail gives a quick over/under of passes and failures of the results in this set
42+
func (vr Results) PassFail() (pass int, fail int) {
43+
for _, res := range vr {
44+
if res.Pass {
45+
pass++
46+
} else {
47+
fail++
48+
}
49+
}
50+
return pass, fail
51+
}

0 commit comments

Comments
 (0)