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  }