github.com/autonomy/conform@v0.1.0-alpha.16/internal/policy/commit/check_conventional_commit.go (about) 1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package commit 6 7 import ( 8 "regexp" 9 "strings" 10 11 "github.com/autonomy/conform/internal/policy" 12 "github.com/pkg/errors" 13 ) 14 15 // Conventional implements the policy.Policy interface and enforces commit 16 // messages to conform the Conventional Commit standard. 17 type Conventional struct { 18 Types []string `mapstructure:"types"` 19 Scopes []string `mapstructure:"scopes"` 20 } 21 22 // HeaderRegex is the regular expression used for Conventional Commits 23 // 1.0.0-beta.1. 24 var HeaderRegex = regexp.MustCompile(`^(\w*)(\(([^)]+)\))?:\s{1}(.*)($|\n{2})`) 25 26 const ( 27 // TypeFeat is a commit of the type fix patches a bug in your codebase 28 // (this correlates with PATCH in semantic versioning). 29 TypeFeat = "feat" 30 31 // TypeFix is a commit of the type feat introduces a new feature to the 32 // codebase (this correlates with MINOR in semantic versioning). 33 TypeFix = "fix" 34 ) 35 36 // ConventionalCommitCheck ensures that the commit message is a valid 37 // conventional commit. 38 type ConventionalCommitCheck struct { 39 errors []error 40 } 41 42 // Name returns the name of the check. 43 func (c ConventionalCommitCheck) Name() string { 44 return "Conventional Commit" 45 } 46 47 // Message returns to check message. 48 func (c ConventionalCommitCheck) Message() string { 49 if len(c.errors) != 0 { 50 return c.errors[0].Error() 51 } 52 return "Commit message is a valid conventional commit" 53 } 54 55 // Errors returns any violations of the check. 56 func (c ConventionalCommitCheck) Errors() []error { 57 return c.errors 58 } 59 60 // ValidateConventionalCommit returns the commit type. 61 // nolint: gocyclo 62 func (c Commit) ValidateConventionalCommit() policy.Check { 63 check := &ConventionalCommitCheck{} 64 groups := parseHeader(c.msg) 65 if len(groups) != 6 { 66 check.errors = append(check.errors, errors.Errorf("Invalid conventional commits format: %q", c.msg)) 67 return check 68 } 69 70 c.Conventional.Types = append(c.Conventional.Types, TypeFeat, TypeFix) 71 typeIsValid := false 72 for _, t := range c.Conventional.Types { 73 if t == groups[1] { 74 typeIsValid = true 75 } 76 } 77 if !typeIsValid { 78 check.errors = append(check.errors, errors.Errorf("Invalid type %q: allowed types are %v", groups[1], c.Conventional.Types)) 79 return check 80 } 81 82 // Scope is optional. 83 if groups[3] != "" { 84 scopeIsValid := false 85 for _, scope := range c.Conventional.Scopes { 86 if scope == groups[3] { 87 scopeIsValid = true 88 break 89 } 90 } 91 if !scopeIsValid { 92 check.errors = append(check.errors, errors.Errorf("Invalid scope %q: allowed scopes are %v", groups[3], c.Conventional.Scopes)) 93 return check 94 } 95 } 96 97 if len(groups[4]) <= 72 && len(groups[4]) != 0 { 98 return check 99 } 100 check.errors = append(check.errors, errors.Errorf("Invalid description: %s", groups[4])) 101 102 return check 103 } 104 105 func parseHeader(msg string) []string { 106 // To circumvent any policy violation due to the leading \n that GitHub 107 // prefixes to the commit message on a squash merge, we remove it from the 108 // message. 109 header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0] 110 groups := HeaderRegex.FindStringSubmatch(header) 111 112 return groups 113 }