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  }