github.com/twelho/conform@v0.0.0-20231016230407-c25e9238598a/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/pkg/errors" 12 13 "github.com/twelho/conform/internal/policy" 14 ) 15 16 // Conventional implements the policy.Policy interface and enforces commit 17 // messages to conform the Conventional Commit standard. 18 type Conventional struct { 19 Types []string `mapstructure:"types"` 20 Scopes []string `mapstructure:"scopes"` 21 DescriptionLength int `mapstructure:"descriptionLength"` 22 } 23 24 // HeaderRegex is the regular expression used for Conventional Commits 1.0.0. 25 var HeaderRegex = regexp.MustCompile(`^(\w*)(\(([^)]+)\))?(!)?:\s{1}(.*)($|\n{2})`) 26 27 const ( 28 // TypeFeat is a commit of the type fix patches a bug in your codebase 29 // (this correlates with PATCH in semantic versioning). 30 TypeFeat = "feat" 31 32 // TypeFix is a commit of the type feat introduces a new feature to the 33 // codebase (this correlates with MINOR in semantic versioning). 34 TypeFix = "fix" 35 ) 36 37 // ConventionalCommitCheck ensures that the commit message is a valid 38 // conventional commit. 39 type ConventionalCommitCheck struct { 40 errors []error 41 } 42 43 // Name returns the name of the check. 44 func (c ConventionalCommitCheck) Name() string { 45 return "Conventional Commit" 46 } 47 48 // Message returns to check message. 49 func (c ConventionalCommitCheck) Message() string { 50 if len(c.errors) != 0 { 51 return c.errors[0].Error() 52 } 53 54 return "Commit message is a valid conventional commit" 55 } 56 57 // Errors returns any violations of the check. 58 func (c ConventionalCommitCheck) Errors() []error { 59 return c.errors 60 } 61 62 // ValidateConventionalCommit returns the commit type. 63 func (c Commit) ValidateConventionalCommit() policy.Check { //nolint:ireturn 64 check := &ConventionalCommitCheck{} 65 groups := parseHeader(c.msg) 66 67 if len(groups) != 7 { 68 check.errors = append(check.errors, errors.Errorf("Invalid conventional commits format: %q", c.msg)) 69 70 return check 71 } 72 73 // conventional commit sections 74 ccType := groups[1] 75 ccScope := groups[3] 76 ccDesc := groups[5] 77 78 c.Conventional.Types = append(c.Conventional.Types, TypeFeat, TypeFix) 79 typeIsValid := false 80 81 for _, t := range c.Conventional.Types { 82 if t == ccType { 83 typeIsValid = true 84 } 85 } 86 87 if !typeIsValid { 88 check.errors = append(check.errors, errors.Errorf("Invalid type %q: allowed types are %v", groups[1], c.Conventional.Types)) 89 90 return check 91 } 92 93 // Scope is optional. 94 if ccScope != "" { 95 scopeIsValid := false 96 97 for _, scope := range c.Conventional.Scopes { 98 re := regexp.MustCompile(scope) 99 if re.Match([]byte(ccScope)) { 100 scopeIsValid = true 101 102 break 103 } 104 } 105 106 if !scopeIsValid { 107 check.errors = append(check.errors, errors.Errorf("Invalid scope %q: allowed scopes are %v", groups[3], c.Conventional.Scopes)) 108 109 return check 110 } 111 } 112 113 // Provide a good default value for DescriptionLength 114 if c.Conventional.DescriptionLength == 0 { 115 c.Conventional.DescriptionLength = 72 116 } 117 118 if len(ccDesc) <= c.Conventional.DescriptionLength && len(ccDesc) != 0 { 119 return check 120 } 121 122 check.errors = append(check.errors, errors.Errorf("Invalid description: %s", ccDesc)) 123 124 return check 125 } 126 127 func parseHeader(msg string) []string { 128 // To circumvent any policy violation due to the leading \n that GitHub 129 // prefixes to the commit message on a squash merge, we remove it from the 130 // message. 131 header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0] 132 groups := HeaderRegex.FindStringSubmatch(header) 133 134 return groups 135 }