Skip to content

Commit

Permalink
feat: Add generic git commit policy (#92)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Rynhard <[email protected]>
  • Loading branch information
andrewrynhard authored Jan 13, 2019
1 parent 76b6d7a commit b59ae9c
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .conform.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
policies:
- type: commit
spec:
headerLength: 89
dco: true
gpg: true
- type: conventionalCommit
spec:
types:
Expand Down
8 changes: 5 additions & 3 deletions internal/enforcer/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

"github.com/autonomy/conform/internal/policy"
"github.com/autonomy/conform/internal/policy/commit"
"github.com/autonomy/conform/internal/policy/conventionalcommit"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
Expand All @@ -27,6 +28,7 @@ type PolicyDeclaration struct {

// policyMap defines the set of policies allowed within Conform.
var policyMap = map[string]policy.Policy{
"commit": &commit.Commit{},
"conventionalCommit": &conventionalcommit.Conventional{},
// "version": &version.Version{},
}
Expand All @@ -51,13 +53,13 @@ func (c *Conform) Enforce(setters ...policy.Option) error {
opts := policy.NewDefaultOptions(setters...)

for _, p := range c.Policies {
fmt.Printf("Enforcing policy %q: ", p.Type)
fmt.Printf("Enforcing policy %q ... ", p.Type)
err := c.enforce(p, opts)
if err != nil {
fmt.Printf("failed\n")
fmt.Printf("FAILED\n")
return err
}
fmt.Printf("pass\n")
fmt.Printf("PASS\n")
}

return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,20 @@ func (g *Git) Message() (message string, err error) {

return message, err
}

// HasGPGSignature returns the commit message. In the case that a commit has multiple
// parents, the message of the last parent is returned.
func (g *Git) HasGPGSignature() (ok bool, err error) {
ref, err := g.repo.Head()
if err != nil {
return
}
commit, err := g.repo.CommitObject(ref.Hash())
if err != nil {
return
}

ok = commit.PGPSignature != ""

return ok, err
}
103 changes: 103 additions & 0 deletions internal/policy/commit/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package commit

import (
"io/ioutil"
"regexp"
"strings"

"github.com/autonomy/conform/internal/git"
"github.com/autonomy/conform/internal/policy"
"github.com/pkg/errors"
)

// Commit implements the policy.Policy interface and enforces commit
// messages to conform the Conventional Commit standard.
type Commit struct {
// HeaderLength is the maximum length of the commit subject.
HeaderLength int `mapstructure:"headerLength"`
// DCO enables the Developer Certificate of Origin check.
DCO bool `mapstructure:"dco"`
// GPG enables the GPG signature check.
GPG bool `mapstructure:"gpg"`
}

// MaxNumberOfCommitCharacters is the default maximium number of characters
// allowed in a commit header.
var MaxNumberOfCommitCharacters = 89

// DCORegex is the regular expression used for Developer Certificate of Origin.
var DCORegex = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`)

// Compliance implements the policy.Policy.Compliance function.
func (c *Commit) Compliance(options *policy.Options) (report policy.Report) {
var err error

report = policy.Report{}

var g *git.Git
if g, err = git.NewGit(); err != nil {
report.Errors = append(report.Errors, errors.Errorf("failed to open git repo: %v", err))
return
}

var msg string
if options.CommitMsgFile != nil {
var contents []byte
if contents, err = ioutil.ReadFile(*options.CommitMsgFile); err != nil {
report.Errors = append(report.Errors, errors.Errorf("failed to read commit message file: %v", err))
return
}
msg = string(contents)
} else {
if msg, err = g.Message(); err != nil {
report.Errors = append(report.Errors, errors.Errorf("failed to get commit message: %v", err))
return
}
}

if c.HeaderLength != 0 {
MaxNumberOfCommitCharacters = c.HeaderLength
}
ValidateHeaderLength(&report, msg)

if c.DCO {
ValidateDCO(&report, msg)
}

if c.GPG {
ValidateGPGSign(&report, g)
}

return report
}

// ValidateHeaderLength checks the header length.
func ValidateHeaderLength(report *policy.Report, msg string) {
header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]
if len(header) > MaxNumberOfCommitCharacters {
report.Errors = append(report.Errors, errors.Errorf("Commit header is %d characters", len(header)))
}
}

// ValidateDCO checks the commit message for a Developer Certificate of Origin.
func ValidateDCO(report *policy.Report, msg string) {
for _, line := range strings.Split(msg, "\n") {
if DCORegex.MatchString(line) {
return
}
}

report.Errors = append(report.Errors, errors.Errorf("Commit does not have a DCO"))
}

// ValidateGPGSign checks the commit message for a GPG signature.
func ValidateGPGSign(report *policy.Report, g *git.Git) {
var err error
var ok bool
if ok, err = g.HasGPGSignature(); !ok {
if err != nil {
report.Errors = append(report.Errors, errors.Errorf("Commit does not have a GPG signature: %v", err))
}
report.Errors = append(report.Errors, errors.Errorf("Commit does not have a GPG signature"))
}
}
14 changes: 5 additions & 9 deletions internal/policy/conventionalcommit/conventionalcommit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"regexp"
"strings"

"github.com/autonomy/conform/internal/git"
"github.com/autonomy/conform/internal/policy"
"github.com/autonomy/conform/internal/policy/conventionalcommit/internal/git"
"github.com/pkg/errors"
)

Expand All @@ -23,7 +23,7 @@ const MaxNumberOfCommitCharacters = 72

// HeaderRegex is the regular expression used for Conventional Commits
// 1.0.0-beta.1.
const HeaderRegex = `^(\w*)(\(([^)]+)\))?:\s{1}(.*)($|\n{2})`
var HeaderRegex = regexp.MustCompile(`^(\w*)(\(([^)]+)\))?:\s{1}(.*)($|\n{2})`)

// TypeFeat is a commit of the type fix patches a bug in your codebase
// (this correlates with PATCH in semantic versioning).
Expand Down Expand Up @@ -110,16 +110,12 @@ func ValidateDescription(report *policy.Report, groups []string) {
report.Errors = append(report.Errors, errors.Errorf("Invalid description: %s", groups[4]))
}

func parseHeader(message string) []string {
re, err := regexp.Compile(HeaderRegex)
if err != nil {
return nil
}
func parseHeader(msg string) []string {
// To circumvent any policy violation due to the leading \n that GitHub
// prefixes to the commit message on a squash merge, we remove it from the
// message.
header := strings.Split(strings.TrimPrefix(message, "\n"), "\n")[0]
groups := re.FindStringSubmatch(header)
header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]
groups := HeaderRegex.FindStringSubmatch(header)

return groups
}

0 comments on commit b59ae9c

Please sign in to comment.