Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ policies:
scopes:
- "scope"
descriptionLength: 72
rejectAutoSquash: false
- type: license
spec:
skipPaths:
Expand Down
33 changes: 27 additions & 6 deletions internal/policy/commit/check_conventional_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ type Conventional struct {
Types []string `mapstructure:"types"`
Scopes []string `mapstructure:"scopes"`
DescriptionLength int `mapstructure:"descriptionLength"`
RejectAutoSquash bool `mapstructure:"rejectAutoSquash"`
}

// HeaderRegex is the regular expression used for Conventional Commits 1.0.0.
var HeaderRegex = regexp.MustCompile(`^(\w*)(\(([^)]+)\))?(!)?:\s{1}(.*)($|\n{2})`)

// AutoSquashHeaderRegex matches git autosquash commit messages.
var AutoSquashHeaderRegex = regexp.MustCompile(`^(fixup|squash|amend)!\s+.+`)

const (
// TypeFeat is a commit of the type fix patches a bug in your codebase
// (this correlates with MINOR in semantic versioning).
Expand Down Expand Up @@ -62,7 +66,19 @@ func (c ConventionalCommitCheck) Errors() []error {
// ValidateConventionalCommit returns the commit type.
func (c Commit) ValidateConventionalCommit() policy.Check { //nolint:ireturn
check := &ConventionalCommitCheck{}
groups := parseHeader(c.msg)
header := firstHeaderLine(c.msg)

if isAutoSquashMessage(header) {
if !c.Conventional.RejectAutoSquash {
return check
}

check.errors = append(check.errors, errors.Errorf("Auto-squash commit format is disabled: %q", c.msg))

return check
}

groups := parseHeader(header)

if len(groups) != 7 {
check.errors = append(check.errors, errors.Errorf("Invalid conventional commits format: %q", c.msg))
Expand Down Expand Up @@ -124,12 +140,17 @@ func (c Commit) ValidateConventionalCommit() policy.Check { //nolint:ireturn
return check
}

func parseHeader(msg string) []string {
// To circumvent any policy violation due to the leading \n that GitHub
// prefixes to the commit message on a squash merge, we remove it from the
// message.
header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]
func parseHeader(header string) []string {
groups := HeaderRegex.FindStringSubmatch(header)

return groups
}

func isAutoSquashMessage(header string) bool {
return AutoSquashHeaderRegex.MatchString(header)
}

func firstHeaderLine(msg string) string {
// GitHub can prefix squash-merge commit messages with a leading newline.
return strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]
}
62 changes: 58 additions & 4 deletions internal/policy/commit/commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestConventionalCommitPolicy(t *testing.T) {
Name string
CreateCommit func(t *testing.T) error
ExpectValid bool
Conventional *Conventional
}

for _, test := range []testDesc{
Expand Down Expand Up @@ -60,6 +61,27 @@ func TestConventionalCommitPolicy(t *testing.T) {
CreateCommit: createInvalidEmptyCommit,
ExpectValid: false,
},
{
Name: "Fixup",
CreateCommit: createFixupCommit,
ExpectValid: true,
},
{
Name: "FixupRejected",
CreateCommit: createFixupCommit,
ExpectValid: false,
Conventional: rejectAutoSquashConventional(),
},
{
Name: "Squash",
CreateCommit: createSquashCommit,
ExpectValid: true,
},
{
Name: "Amend",
CreateCommit: createAmendCommit,
ExpectValid: true,
},
} {
func(test testDesc) {
t.Run(test.Name, func(tt *testing.T) {
Expand All @@ -77,7 +99,7 @@ func TestConventionalCommitPolicy(t *testing.T) {
tt.Error(err)
}

report, err := runCompliance()
report, err := runComplianceWithConventional(test.Conventional)
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -363,16 +385,30 @@ func runComplianceRange(id1, id2 string) (*policy.Report, error) {
}

func runCompliance() (*policy.Report, error) {
c := &Commit{
Conventional: &Conventional{
return runComplianceWithConventional(nil)
}

func runComplianceWithConventional(conventional *Conventional) (*policy.Report, error) {
if conventional == nil {
conventional = &Conventional{
Types: []string{"type"},
Scopes: []string{"scope", "^valid"},
},
}
}

c := &Commit{Conventional: conventional}

return c.Compliance(&policy.Options{})
}

func rejectAutoSquashConventional() *Conventional {
return &Conventional{
Types: []string{"type"},
Scopes: []string{"scope", "^valid"},
RejectAutoSquash: true,
}
}

func initRepo(t *testing.T) error {
_, err := exec.CommandContext(t.Context(), "git", "init").Output()
if err != nil {
Expand Down Expand Up @@ -442,3 +478,21 @@ func createInvalidCommitRegex(t *testing.T) error {

return err
}

func createFixupCommit(t *testing.T) error {
_, err := exec.CommandContext(t.Context(), "git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "fixup! deadbeef").Output()

return err
}

func createSquashCommit(t *testing.T) error {
_, err := exec.CommandContext(t.Context(), "git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "squash! deadbeef").Output()

return err
}

func createAmendCommit(t *testing.T) error {
_, err := exec.CommandContext(t.Context(), "git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "amend! deadbeef").Output()

return err
}