Initial commit

Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
This commit is contained in:
Vincent Batts 2015-10-05 16:16:52 -04:00
commit c10ba9c097
7 changed files with 307 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*~
git-validation

21
LICENSE Normal file
View File

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

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# git-validation
A way to do per git commit validation

105
git/commits.go Normal file
View File

@ -0,0 +1,105 @@
package git
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"strings"
)
// Commits returns a set of commits.
// If commitrange is a git still range 12345...54321, then it will be isolated set of commits.
// If commitrange is a single commit, all ancestor commits up through the hash provided.
func Commits(commitrange string) ([]CommitEntry, error) {
output, err := exec.Command("git", "log", prettyFormat+formatCommit, commitrange).Output()
if err != nil {
return nil, err
}
commitHashes := strings.Split(strings.TrimSpace(string(output)), "\n")
commits := make([]CommitEntry, len(commitHashes))
for i, commitHash := range commitHashes {
c, err := LogCommit(commitHash)
if err != nil {
return commits, err
}
commits[i] = *c
}
return commits, nil
}
// CommitEntry represents a single commit's information from `git`
type CommitEntry map[string]string
var (
prettyFormat = `--pretty=format:`
formatSubject = `%s`
formatBody = `%b`
formatCommit = `%H`
formatAuthorName = `%aN`
formatAuthorEmail = `%aE`
formatCommitterName = `%cN`
formatCommitterEmail = `%cE`
formatSigner = `%GS`
formatCommitNotes = `%N`
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" }`
)
// LogCommit assembles the full information on a commit from its commit hash
func LogCommit(commit string) (*CommitEntry, error) {
buf := bytes.NewBuffer([]byte{})
cmd := exec.Command("git", "log", "-1", prettyFormat+formatMap, commit)
cmd.Stdout = buf
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Println(strings.Join(cmd.Args, " "))
return nil, err
}
c := CommitEntry{}
output := buf.Bytes()
if err := json.Unmarshal(output, &c); err != nil {
fmt.Println(string(output))
return nil, err
}
// any user provided fields can't be sanitized for the mock-json marshal above
for k, v := range map[string]string{
"subject": formatSubject,
"body": formatBody,
"author_name": formatAuthorName,
"author_email": formatAuthorEmail,
"committer_name": formatCommitterName,
"committer_email": formatCommitterEmail,
"commit_notes": formatCommitNotes,
"signer": formatSigner,
} {
output, err := exec.Command("git", "log", "-1", prettyFormat+v, commit).Output()
if err != nil {
return nil, err
}
c[k] = strings.TrimSpace(string(output))
}
return &c, nil
}
// FetchHeadCommit returns the hash of FETCH_HEAD
func FetchHeadCommit() (string, error) {
output, err := exec.Command("git", "rev-parse", "--verify", "FETCH_HEAD").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
// HeadCommit returns the hash of HEAD
func HeadCommit() (string, error) {
output, err := exec.Command("git", "rev-parse", "--verify", "HEAD").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}

76
main.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/vbatts/git-validation/git"
_ "github.com/vbatts/git-validation/rules/dco"
"github.com/vbatts/git-validation/validate"
)
var (
flCommitRange = flag.String("range", "", "use this commit range instead")
flListRules = flag.Bool("list-rules", false, "list the rules registered")
flRun = flag.String("run", "", "comma delimited list of rules to run. Defaults to all.")
flVerbose = flag.Bool("v", false, "verbose")
)
func main() {
flag.Parse()
if *flListRules {
for _, r := range validate.RegisteredRules {
fmt.Printf("%q -- %s\n", r.Name, r.Description)
}
return
}
var commitrange string
if *flCommitRange != "" {
commitrange = *flCommitRange
} else {
var err error
commitrange, err = git.FetchHeadCommit()
if err != nil {
log.Fatal(err)
}
}
c, err := git.Commits(commitrange)
if err != nil {
log.Fatal(err)
}
results := validate.Results{}
for _, commit := range c {
fmt.Printf(" * %s %s ... ", commit["abbreviated_commit"], commit["subject"])
vr := validate.Commit(commit, validate.RegisteredRules)
results = append(results, vr...)
if _, fail := vr.PassFail(); fail == 0 {
fmt.Println("PASS")
if *flVerbose {
for _, r := range vr {
if r.Pass {
fmt.Printf(" - %s\n", r.Msg)
}
}
}
} else {
fmt.Println("FAIL")
// default, only print out failed validations
for _, r := range vr {
if !r.Pass {
fmt.Printf(" - %s\n", r.Msg)
}
}
}
}
_, fail := results.PassFail()
if fail > 0 {
fmt.Printf("%d issues to fix\n", fail)
os.Exit(1)
}
}

47
rules/dco/dco.go Normal file
View File

@ -0,0 +1,47 @@
package dco
import (
"regexp"
"strings"
"github.com/vbatts/git-validation/git"
"github.com/vbatts/git-validation/validate"
)
func init() {
validate.RegisterRule(DcoRule)
}
var (
ValidDCO = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`)
DcoRule = validate.Rule{
Name: "DCO",
Description: "makes sure the commits are signed",
Run: ValidateDCO,
}
)
func ValidateDCO(c git.CommitEntry) (vr validate.Result) {
vr.CommitEntry = c
if len(strings.Split(c["parent"], " ")) > 1 {
vr.Pass = true
vr.Msg = "merge commits do not require DCO"
return vr
}
hasValid := false
for _, line := range strings.Split(c["body"], "\n") {
if ValidDCO.MatchString(line) {
hasValid = true
}
}
if !hasValid {
vr.Pass = false
vr.Msg = "does not have a valid DCO"
} else {
vr.Pass = true
vr.Msg = "has a valid DCO"
}
return vr
}

51
validate/rules.go Normal file
View File

@ -0,0 +1,51 @@
package validate
import "github.com/vbatts/git-validation/git"
var (
// RegisteredRules are the standard validation to perform on git commits
RegisteredRules = []Rule{}
)
// RegisterRule includes the Rule in the avaible set to use
func RegisterRule(vr Rule) {
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
Description string // longer Description for readability
Run func(git.CommitEntry) Result
}
// 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))
}
return results
}
// Result is the result for a single validation of a commit.
type Result struct {
CommitEntry git.CommitEntry
Pass bool
Msg string
}
// Results is a set of results. This is type makes it easy for the following function.
type Results []Result
// PassFail gives a quick over/under of passes and failures of the results in this set
func (vr Results) PassFail() (pass int, fail int) {
for _, res := range vr {
if res.Pass {
pass++
} else {
fail++
}
}
return pass, fail
}