Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(policy): add imperative mood check #108

Merged
merged 1 commit into from
Jan 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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