Skip to content

Commit 2d9842b

Browse files
committed
Merge pull request #167 from vbatts/validate-dco
.tools: repo validation tool
2 parents 9a8748c + 8b55acf commit 2d9842b

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

.tools/validate.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"os"
10+
"os/exec"
11+
"regexp"
12+
"strings"
13+
)
14+
15+
// DefaultRules are the standard validation to perform on git commits
16+
var DefaultRules = []ValidateRule{
17+
func(c CommitEntry) (vr ValidateResult) {
18+
vr.CommitEntry = c
19+
hasValid := false
20+
for _, line := range strings.Split(c.Body, "\n") {
21+
if validDCO.MatchString(line) {
22+
hasValid = true
23+
}
24+
}
25+
if !hasValid {
26+
vr.Pass = false
27+
vr.Msg = "does not have a valid DCO"
28+
} else {
29+
vr.Pass = true
30+
vr.Msg = "has a valid DCO"
31+
}
32+
33+
return vr
34+
},
35+
// TODO add something for the cleanliness of the c.Subject
36+
}
37+
38+
var (
39+
flVerbose = flag.Bool("v", false, "verbose")
40+
flCommitRange = flag.String("range", "", "use this commit range instead")
41+
42+
validDCO = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`)
43+
)
44+
45+
func main() {
46+
flag.Parse()
47+
48+
var commitrange string
49+
if *flCommitRange != "" {
50+
commitrange = *flCommitRange
51+
} else {
52+
var err error
53+
commitrange, err = GitFetchHeadCommit()
54+
if err != nil {
55+
log.Fatal(err)
56+
}
57+
}
58+
59+
c, err := GitCommits(commitrange)
60+
if err != nil {
61+
log.Fatal(err)
62+
}
63+
64+
results := ValidateResults{}
65+
for _, commit := range c {
66+
fmt.Printf(" * %s %s ... ", commit.AbbreviatedCommit, commit.Subject)
67+
vr := ValidateCommit(commit, DefaultRules)
68+
results = append(results, vr...)
69+
if _, fail := vr.PassFail(); fail == 0 {
70+
fmt.Println("PASS")
71+
if *flVerbose {
72+
for _, r := range vr {
73+
if !r.Pass {
74+
fmt.Printf(" - %s\n", r.Msg)
75+
}
76+
}
77+
}
78+
} else {
79+
fmt.Println("FAIL")
80+
// default, only print out failed validations
81+
for _, r := range vr {
82+
if !r.Pass {
83+
fmt.Printf(" - %s\n", r.Msg)
84+
}
85+
}
86+
}
87+
}
88+
_, fail := results.PassFail()
89+
if fail > 0 {
90+
fmt.Printf("%d issues to fix\n", fail)
91+
os.Exit(1)
92+
}
93+
}
94+
95+
// ValidateRule will operate over a provided CommitEntry, and return a result.
96+
type ValidateRule func(CommitEntry) ValidateResult
97+
98+
// ValidateCommit processes the given rules on the provided commit, and returns the result set.
99+
func ValidateCommit(c CommitEntry, rules []ValidateRule) ValidateResults {
100+
results := ValidateResults{}
101+
for _, r := range rules {
102+
results = append(results, r(c))
103+
}
104+
return results
105+
}
106+
107+
// ValidateResult is the result for a single validation of a commit.
108+
type ValidateResult struct {
109+
CommitEntry CommitEntry
110+
Pass bool
111+
Msg string
112+
}
113+
114+
// ValidateResults is a set of results. This is type makes it easy for the following function.
115+
type ValidateResults []ValidateResult
116+
117+
// PassFail gives a quick over/under of passes and failures of the results in this set
118+
func (vr ValidateResults) PassFail() (pass int, fail int) {
119+
for _, res := range vr {
120+
if res.Pass {
121+
pass++
122+
} else {
123+
fail++
124+
}
125+
}
126+
return pass, fail
127+
}
128+
129+
// CommitEntry represents a single commit's information from `git`
130+
type CommitEntry struct {
131+
Commit string
132+
AbbreviatedCommit string `json:"abbreviated_commit"`
133+
Tree string
134+
AbbreviatedTree string `json:"abbreviated_tree"`
135+
Parent string
136+
AbbreviatedParent string `json:"abbreviated_parent"`
137+
Refs string
138+
Encoding string
139+
Subject string
140+
SanitizedSubjectLine string `json:"sanitized_subject_line"`
141+
Body string
142+
CommitNotes string `json:"commit_notes"`
143+
VerificationFlag string `json:"verification_flag"`
144+
ShortMsg string
145+
Signer string
146+
SignerKey string `json:"signer_key"`
147+
Author PersonAction `json:"author,omitempty"`
148+
Commiter PersonAction `json:"commiter,omitempty"`
149+
}
150+
151+
// PersonAction is a time and identity of an action on a git commit
152+
type PersonAction struct {
153+
Name string `json:"name"`
154+
Email string `json:"email"`
155+
Date string `json:"date"` // this could maybe be an actual time.Time
156+
}
157+
158+
var (
159+
prettyLogSubject = `--pretty=format:%s`
160+
prettyLogBody = `--pretty=format:%b`
161+
prettyLogCommit = `--pretty=format:%H`
162+
prettyLogAuthorName = `--pretty=format:%aN`
163+
prettyLogAuthorEmail = `--pretty=format:%aE`
164+
prettyLogCommiterName = `--pretty=format:%cN`
165+
prettyLogCommiterEmail = `--pretty=format:%cE`
166+
prettyLogSigner = `--pretty=format:%GS`
167+
prettyLogCommitNotes = `--pretty=format:%N`
168+
prettyLogFormat = `--pretty=format:{"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" }, "commiter": { "date": "%cD" }}`
169+
)
170+
171+
// GitLogCommit assembles the full information on a commit from its commit hash
172+
func GitLogCommit(commit string) (*CommitEntry, error) {
173+
buf := bytes.NewBuffer([]byte{})
174+
cmd := exec.Command("git", "log", "-1", prettyLogFormat, commit)
175+
cmd.Stdout = buf
176+
cmd.Stderr = os.Stderr
177+
178+
if err := cmd.Run(); err != nil {
179+
log.Println(strings.Join(cmd.Args, " "))
180+
return nil, err
181+
}
182+
c := CommitEntry{}
183+
output := buf.Bytes()
184+
if err := json.Unmarshal(output, &c); err != nil {
185+
fmt.Println(string(output))
186+
return nil, err
187+
}
188+
189+
output, err := exec.Command("git", "log", "-1", prettyLogSubject, commit).Output()
190+
if err != nil {
191+
return nil, err
192+
}
193+
c.Subject = strings.TrimSpace(string(output))
194+
195+
output, err = exec.Command("git", "log", "-1", prettyLogBody, commit).Output()
196+
if err != nil {
197+
return nil, err
198+
}
199+
c.Body = strings.TrimSpace(string(output))
200+
201+
output, err = exec.Command("git", "log", "-1", prettyLogAuthorName, commit).Output()
202+
if err != nil {
203+
return nil, err
204+
}
205+
c.Author.Name = strings.TrimSpace(string(output))
206+
207+
output, err = exec.Command("git", "log", "-1", prettyLogAuthorEmail, commit).Output()
208+
if err != nil {
209+
return nil, err
210+
}
211+
c.Author.Email = strings.TrimSpace(string(output))
212+
213+
output, err = exec.Command("git", "log", "-1", prettyLogCommiterName, commit).Output()
214+
if err != nil {
215+
return nil, err
216+
}
217+
c.Commiter.Name = strings.TrimSpace(string(output))
218+
219+
output, err = exec.Command("git", "log", "-1", prettyLogCommiterEmail, commit).Output()
220+
if err != nil {
221+
return nil, err
222+
}
223+
c.Commiter.Email = strings.TrimSpace(string(output))
224+
225+
output, err = exec.Command("git", "log", "-1", prettyLogCommitNotes, commit).Output()
226+
if err != nil {
227+
return nil, err
228+
}
229+
c.CommitNotes = strings.TrimSpace(string(output))
230+
231+
output, err = exec.Command("git", "log", "-1", prettyLogSigner, commit).Output()
232+
if err != nil {
233+
return nil, err
234+
}
235+
c.Signer = strings.TrimSpace(string(output))
236+
237+
return &c, nil
238+
}
239+
240+
// GitCommits returns a set of commits.
241+
// If commitrange is a git still range 12345...54321, then it will be isolated set of commits.
242+
// If commitrange is a single commit, all ancestor commits up through the hash provided.
243+
func GitCommits(commitrange string) ([]CommitEntry, error) {
244+
output, err := exec.Command("git", "log", prettyLogCommit, commitrange).Output()
245+
if err != nil {
246+
return nil, err
247+
}
248+
commitHashes := strings.Split(strings.TrimSpace(string(output)), "\n")
249+
commits := make([]CommitEntry, len(commitHashes))
250+
for i, commitHash := range commitHashes {
251+
c, err := GitLogCommit(commitHash)
252+
if err != nil {
253+
return commits, err
254+
}
255+
commits[i] = *c
256+
}
257+
return commits, nil
258+
}
259+
260+
// GitFetchHeadCommit returns the hash of FETCH_HEAD
261+
func GitFetchHeadCommit() (string, error) {
262+
output, err := exec.Command("git", "rev-parse", "--verify", "HEAD").Output()
263+
if err != nil {
264+
return "", err
265+
}
266+
return strings.TrimSpace(string(output)), nil
267+
}
268+
269+
// GitHeadCommit returns the hash of HEAD
270+
func GitHeadCommit() (string, error) {
271+
output, err := exec.Command("git", "rev-parse", "--verify", "HEAD").Output()
272+
if err != nil {
273+
return "", err
274+
}
275+
return strings.TrimSpace(string(output)), nil
276+
}

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ install: true
1414
script:
1515
- go vet -x ./...
1616
- $HOME/gopath/bin/golint ./...
17+
- go run .tools/validate.go -range ${TRAVIS_COMMIT_RANGE}
1718

0 commit comments

Comments
 (0)