Skip to content

Commit

Permalink
feat(policy): add checks for header case and last character
Browse files Browse the repository at this point in the history
It's common for commit message guidelines to include restrictions on the
leading case and trailing punctuation of the header; this adds checks to
enforce those. The desired case can be specified as either "upper" or
"lower"; the punctuation check is implemented as a general check for
disallowing the last character in the header to be from any given set of
characters.

Now that there are several header-related checks, this also adds a level
to the config to put all of those checks in one place in the
configuration.

Signed-off-by: Danny Zhu <[email protected]>
  • Loading branch information
dzhu authored and andrewrynhard committed Mar 30, 2020
1 parent a55d411 commit fa7df19
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 36 deletions.
7 changes: 5 additions & 2 deletions .conform.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
policies:
- type: commit
spec:
headerLength: 89
header:
length: 89
imperative: true
case: lower
invalidLastCharacters: .
dco: true
gpg: false
imperative: true
maximumOfOneCommit: true
requireCommitBody: true
conventional:
Expand Down
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ Now, create a file named `.conform.yaml` with the following contents:
policies:
- type: commit
spec:
headerLength: 89
header:
length: 89
imperative: true
case: lower
invalidLastCharacters: .
dco: true
gpg: false
imperative: true
maximumOfOneCommit: true
requireCommitBody: true
conventional:
Expand All @@ -71,14 +74,16 @@ In the same directory, run:
```bash
$ conform enforce
POLICY CHECK STATUS MESSAGE
commit Header Length PASS <none>
commit DCO PASS <none>
commit Imperative Mood PASS <none>
commit Conventional Commit PASS <none>
commit Number of Commits PASS <none>
commit Commit Body PASS <none>
license File Header PASS <none>
POLICY CHECK STATUS MESSAGE
commit Header Length PASS <none>
commit Imperative Mood PASS <none>
commit Header Case PASS <none>
commit Header Last Character PASS <none>
commit DCO PASS <none>
commit Conventional Commit PASS <none>
commit Number of Commits PASS <none>
commit Commit Body PASS <none>
license File Header PASS <none>
```

To setup a `commit-msg` hook:
Expand Down
4 changes: 0 additions & 4 deletions internal/policy/commit/check_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ func (h Body) Errors() []error {
func (c Commit) ValidateBody() policy.Check {
check := &Body{}

if c.HeaderLength != 0 {
MaxNumberOfCommitCharacters = c.HeaderLength
}

lines := strings.Split(strings.TrimPrefix(c.msg, "\n"), "\n")
valid := false
for _, line := range lines[1:] {
Expand Down
69 changes: 69 additions & 0 deletions internal/policy/commit/check_header_case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* 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

import (
"unicode"
"unicode/utf8"

"github.com/pkg/errors"
"github.com/talos-systems/conform/internal/policy"
)

// HeaderCaseCheck enforces the case of the first word in the header.
type HeaderCaseCheck struct {
headerCase string
errors []error
}

// Name returns the name of the check.
func (h HeaderCaseCheck) Name() string {
return "Header Case"
}

// Message returns to check message.
func (h HeaderCaseCheck) Message() string {
if len(h.errors) != 0 {
return h.errors[0].Error()
}
return "Header case is valid"
}

// Errors returns any violations of the check.
func (h HeaderCaseCheck) Errors() []error {
return h.errors
}

// ValidateHeaderCase checks the header length.
func (c Commit) ValidateHeaderCase() policy.Check {
check := &HeaderCaseCheck{headerCase: c.Header.Case}

firstWord, err := c.firstWord()
if err != nil {
check.errors = append(check.errors, err)
return check
}

first, _ := utf8.DecodeRuneInString(firstWord)
if first == utf8.RuneError {
check.errors = append(check.errors, errors.New("Header does not start with valid UTF-8 text"))
return check
}

var valid bool
switch c.Header.Case {
case "upper":
valid = unicode.IsUpper(first)
case "lower":
valid = unicode.IsLower(first)
default:
check.errors = append(check.errors, errors.Errorf("Invalid configured case %s", c.Header.Case))
return check
}
if !valid {
check.errors = append(check.errors, errors.Errorf("Commit header case is not %s", c.Header.Case))
}
return check
}
50 changes: 50 additions & 0 deletions internal/policy/commit/check_header_last_character.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* 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

import (
"strings"
"unicode/utf8"

"github.com/pkg/errors"
"github.com/talos-systems/conform/internal/policy"
)

// HeaderLastCharacterCheck enforces that the last character of the header isn't in some set.
type HeaderLastCharacterCheck struct {
errors []error
}

// Name returns the name of the check.
func (h HeaderLastCharacterCheck) Name() string {
return "Header Last Character"
}

// Message returns to check message.
func (h HeaderLastCharacterCheck) Message() string {
if len(h.errors) != 0 {
return h.errors[0].Error()
}
return "Header last character is valid"
}

// Errors returns any violations of the check.
func (h HeaderLastCharacterCheck) Errors() []error {
return h.errors
}

// ValidateHeaderLastCharacter checks the last character of the header.
func (c Commit) ValidateHeaderLastCharacter() policy.Check {
check := &HeaderLastCharacterCheck{}

switch last, _ := utf8.DecodeLastRuneInString(c.header()); {
case last == utf8.RuneError:
check.errors = append(check.errors, errors.New("Header does not end with valid UTF-8 text"))
case strings.ContainsRune(c.Header.InvalidLastCharacters, last):
check.errors = append(check.errors, errors.Errorf("Commit header ends in %q", last))
}

return check
}
10 changes: 4 additions & 6 deletions internal/policy/commit/check_header_length.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package commit

import (
"fmt"
"strings"

"github.com/pkg/errors"
"github.com/talos-systems/conform/internal/policy"
Expand Down Expand Up @@ -42,14 +41,13 @@ func (h HeaderLengthCheck) Errors() []error {
func (c Commit) ValidateHeaderLength() policy.Check {
check := &HeaderLengthCheck{}

if c.HeaderLength != 0 {
MaxNumberOfCommitCharacters = c.HeaderLength
if c.Header.Length != 0 {
MaxNumberOfCommitCharacters = c.Header.Length
}

header := strings.Split(strings.TrimPrefix(c.msg, "\n"), "\n")[0]
check.headerLength = len(header)
check.headerLength = len(c.header())
if check.headerLength > MaxNumberOfCommitCharacters {
check.errors = append(check.errors, errors.Errorf("Commit header is %d characters", len(header)))
check.errors = append(check.errors, errors.Errorf("Commit header is %d characters", check.headerLength))
}

return check
Expand Down
51 changes: 37 additions & 14 deletions internal/policy/commit/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,35 @@ import (
"github.com/talos-systems/conform/internal/policy"
)

// HeaderChecks is the configuration for checks on the header of a commit.
type HeaderChecks struct {
// Length is the maximum length of the commit subject.
Length int `mapstructure:"length"`
// Imperative enforces the use of imperative verbs as the first word of a
// commit message.
Imperative bool `mapstructure:"imperative"`
// HeaderCase is the case that the first word of the header must have ("upper" or "lower").
Case string `mapstructure:"case"`
// HeaderInvalidLastCharacters is a string containing all invalid last characters for the header.
InvalidLastCharacters string `mapstructure:"invalidLastCharacters"`
}

// 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"`
// Imperative enforces the use of imperative verbs as the first word of a
// commit message.
Imperative bool `mapstructure:"imperative"`
// MaximumOfOneCommit enforces that the current commit is only one commit
// ahead of a specified ref.
MaximumOfOneCommit bool `mapstructure:"maximumOfOneCommit"`
// RequireCommitBody enforces that the current commit has a body.
RequireCommitBody bool `mapstructure:"requireCommitBody"`
// Conventional is the user specified settings for conventional commits.
Conventional *Conventional `mapstructure:"conventional"`
// Header is the user specified settings for the header of each commit.
Header *HeaderChecks `mapstructure:"header"`

msg string
}
Expand Down Expand Up @@ -67,8 +77,22 @@ func (c *Commit) Compliance(options *policy.Options) (*policy.Report, error) {
}
c.msg = msg

if c.HeaderLength != 0 {
report.AddCheck(c.ValidateHeaderLength())
if c.Header != nil {
if c.Header.Length != 0 {
report.AddCheck(c.ValidateHeaderLength())
}

if c.Header.Imperative {
report.AddCheck(c.ValidateImperative())
}

if c.Header.Case != "" {
report.AddCheck(c.ValidateHeaderCase())
}

if c.Header.InvalidLastCharacters != "" {
report.AddCheck(c.ValidateHeaderLastCharacter())
}
}

if c.DCO {
Expand All @@ -79,10 +103,6 @@ func (c *Commit) Compliance(options *policy.Options) (*policy.Report, error) {
report.AddCheck(c.ValidateGPGSign(g))
}

if c.Imperative {
report.AddCheck(c.ValidateImperative())
}

if c.Conventional != nil {
report.AddCheck(c.ValidateConventionalCommit())
}
Expand All @@ -99,7 +119,6 @@ func (c *Commit) Compliance(options *policy.Options) (*policy.Report, error) {
}

func (c Commit) firstWord() (string, error) {
var header string
var groups []string
var msg string
if c.Conventional != nil {
Expand All @@ -111,11 +130,15 @@ func (c Commit) firstWord() (string, error) {
} else {
msg = c.msg
}
if header = strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]; header == "" {
if msg == "" {
return "", errors.Errorf("Invalid msg: %s", msg)
}
if groups = FirstWordRegex.FindStringSubmatch(header); groups == nil {
if groups = FirstWordRegex.FindStringSubmatch(msg); groups == nil {
return "", errors.Errorf("Invalid msg: %s", msg)
}
return groups[0], nil
}

func (c Commit) header() string {
return strings.Split(strings.TrimPrefix(c.msg, "\n"), "\n")[0]
}

0 comments on commit fa7df19

Please sign in to comment.