Skip to content

Commit

Permalink
feat(policy): add imperative mood check (#108)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Rynhard <[email protected]>
  • Loading branch information
andrewrynhard authored Jan 21, 2019
1 parent 86a7d3e commit 5c6620a
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 141 deletions.
23 changes: 11 additions & 12 deletions .conform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ policies:
headerLength: 89
dco: true
gpg: true
- type: conventionalCommit
spec:
types:
- chore
- docs
- perf
- refactor
- style
- test
scopes:
- policy
- '*'
imperative: true
conventional:
types:
- chore
- docs
- perf
- refactor
- style
- test
scopes:
- policy
- type: license
spec:
includeSuffixes:
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

Some of the policies included are:

- **Commits**: Enforce basic commit policies including:
- **Commits**: Enforce commit policies including:
- Commit message header length
- Developer Certificate of Origin
- GPG signature
- **Conventional Commits**: Enforce [conventional commits](https://www.conventionalcommits.org) for all commit messages.
- [Conventional Commits](https://www.conventionalcommits.org)
- Imperative verb
- **License Headers**: Enforce license headers on source code files.

## Getting Started
Expand All @@ -36,12 +37,12 @@ policies:
headerLength: 89
dco: true
gpg: true
- type: conventionalCommit
spec:
types:
- "type"
scopes:
- "scope"
imperative: true
conventional:
types:
- "type"
scopes:
- "scope"
- type: license
spec:
includeSuffixes:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20170525151105-fa48d7ff1cfb // indirect
github.com/kljensen/snowball v0.6.0
github.com/kr/pretty v0.1.0 // indirect
github.com/magiconair/properties v1.7.2 // indirect
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v0.0.0-20170525151105-fa48d7ff1cfb h1:4qB7kGgjot2tlCOW66sJ+ai5tv81oIDM9t6cvyFTKLM=
github.com/kevinburke/ssh_config v0.0.0-20170525151105-fa48d7ff1cfb/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kljensen/snowball v0.6.0 h1:6DZLCcZeL0cLfodx+Md4/OLC6b/bfurWUOUGs1ydfOU=
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
6 changes: 2 additions & 4 deletions internal/enforcer/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (

"github.com/autonomy/conform/internal/policy"
"github.com/autonomy/conform/internal/policy/commit"
"github.com/autonomy/conform/internal/policy/conventionalcommit"
"github.com/autonomy/conform/internal/policy/license"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
Expand All @@ -34,9 +33,8 @@ type PolicyDeclaration struct {

// policyMap defines the set of policies allowed within Conform.
var policyMap = map[string]policy.Policy{
"commit": &commit.Commit{},
"conventionalCommit": &conventionalcommit.Conventional{},
"license": &license.License{},
"commit": &commit.Commit{},
"license": &license.License{},
// "version": &version.Version{},
}

Expand Down
84 changes: 84 additions & 0 deletions internal/policy/commit/blacklist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package commit

// BlackList is the set of black listed imperative verbs.
var BlackList = []string{
"a",
"an",
"the",
"action",
"always",
"api",
"base",
"basic",
"business",
"calculation",
"callback",
"collection",
"common",
"constructor",
"convenience",
"convenient",
"current",
"currently",
"custom",
"data",
"data",
"default",
"deprecated",
"description",
"dict",
"dictionary",
"does",
"dummy",
"example",
"factory",
"false",
"final",
"formula",
"function",
"generic",
"handler",
"handler",
"helper",
"here",
"hook",
"implementation",
"importantly",
"internal",
"it",
"main",
"method",
"module",
"new",
"number",
"optional",
"package",
"placeholder",
"reference",
"result",
"same",
"schema",
"setup",
"should",
"simple",
"some",
"special",
"sql",
"standard",
"static",
"string",
"subclasses",
"that",
"these",
"this",
"true",
"unique",
"unit",
"utility",
"what",
"wrapper",
}
121 changes: 121 additions & 0 deletions internal/policy/commit/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

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

Expand All @@ -23,6 +24,18 @@ type Commit struct {
DCO bool `mapstructure:"dco"`
// GPG enables the GPG signature check.
GPG bool `mapstructure:"gpg"`
// Imperative enforces the use of imperative verbs as the first word of a
// commit message.
Imperative bool `mapstructure:"imperative"`
// Conventional is the user specified settings for conventional commits.
Conventional *Conventional `mapstructure:"conventional"`
}

// Conventional implements the policy.Policy interface and enforces commit
// messages to conform the Conventional Commit standard.
type Conventional struct {
Types []string `mapstructure:"types"`
Scopes []string `mapstructure:"scopes"`
}

// MaxNumberOfCommitCharacters is the default maximium number of characters
Expand All @@ -32,7 +45,26 @@ var MaxNumberOfCommitCharacters = 89
// DCORegex is the regular expression used for Developer Certificate of Origin.
var DCORegex = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`)

// FirstWordRegex is theregular expression used to find the first word in a
// commit.
var FirstWordRegex = regexp.MustCompile(`^\s*([a-zA-Z0-9]+)`)

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

const (
// TypeFeat is a commit of the type fix patches a bug in your codebase
// (this correlates with PATCH in semantic versioning).
TypeFeat = "feat"

// TypeFix is a commit of the type feat introduces a new feature to the
// codebase (this correlates with MINOR in semantic versioning).
TypeFix = "fix"
)

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

Expand Down Expand Up @@ -72,6 +104,25 @@ func (c *Commit) Compliance(options *policy.Options) (report policy.Report) {
ValidateGPGSign(&report, g)
}

word := firstWord(msg)

if c.Conventional != nil {
groups := parseHeader(msg)
if len(groups) != 6 {
report.Errors = append(report.Errors, errors.Errorf("Invalid conventional commits format: %s", msg))
return
}
word = firstWord(groups[4])

ValidateType(&report, groups, c.Conventional.Types)
ValidateScope(&report, groups, c.Conventional.Scopes)
ValidateDescription(&report, groups)
}

if c.Imperative {
ValidateImperative(&report, word)
}

return report
}

Expand Down Expand Up @@ -105,3 +156,73 @@ func ValidateGPGSign(report *policy.Report, g *git.Git) {
report.Errors = append(report.Errors, errors.Errorf("Commit does not have a GPG signature"))
}
}

// ValidateImperative checks the commit message for a GPG signature.
func ValidateImperative(report *policy.Report, word string) {
word = strings.ToLower(word)
for _, good := range WhiteList {
stemmed, err := snowball.Stem(good, "english", true)
if err != nil {
report.Errors = append(report.Errors, errors.Errorf("Error checking for imperative mood: %v", err))
}
if word == stemmed {
return
}
}
for _, bad := range BlackList {
if word == bad {
report.Errors = append(report.Errors, errors.Errorf("Use of blacklisted imperative verb: %s", word))
return
}
}
report.Errors = append(report.Errors, errors.Errorf("Commit does not have imperative mood"))
}

// ValidateType returns the commit type.
func ValidateType(report *policy.Report, groups []string, types []string) {
types = append(types, TypeFeat, TypeFix)
for _, t := range types {
if t == groups[1] {
return
}
}
report.Errors = append(report.Errors, errors.Errorf("Invalid type: %s, allowed types are: %v", groups[1], types))
}

// ValidateScope returns the commit scope.
func ValidateScope(report *policy.Report, groups []string, scopes []string) {
// Scope is optional.
if groups[3] == "" {
return
}
for _, scope := range scopes {
if scope == groups[3] {
return
}
}
report.Errors = append(report.Errors, errors.Errorf("Invalid scope: %s, allowed scopes are: %v", groups[3], scopes))
}

// ValidateDescription returns the commit description.
func ValidateDescription(report *policy.Report, groups []string) {
if len(groups[4]) <= 72 && len(groups[4]) != 0 {
return
}
report.Errors = append(report.Errors, errors.Errorf("Invalid description: %s", groups[4]))
}

func firstWord(msg string) string {
header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]
groups := FirstWordRegex.FindStringSubmatch(header)
return groups[0]
}

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(msg, "\n"), "\n")[0]
groups := HeaderRegex.FindStringSubmatch(header)

return groups
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package conventionalcommit
package commit

import (
"io/ioutil"
Expand Down Expand Up @@ -76,9 +76,12 @@ func TestInvalidConventionalCommitPolicy(t *testing.T) {
}

func runCompliance() (*policy.Report, error) {
c := &Conventional{}
c.Types = []string{"type"}
c.Scopes = []string{"scope"}
c := &Commit{
Conventional: &Conventional{
Types: []string{"type"},
Scopes: []string{"scope"},
},
}

report := c.Compliance(&policy.Options{})

Expand Down
Loading

0 comments on commit 5c6620a

Please sign in to comment.