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
acceptAutoSquash: false
- type: license
spec:
skipPaths:
Expand Down
35 changes: 29 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,17 @@ type Conventional struct {
Types []string `mapstructure:"types"`
Scopes []string `mapstructure:"scopes"`
DescriptionLength int `mapstructure:"descriptionLength"`
AcceptAutoSquash bool `mapstructure:"acceptAutoSquash"`
}

// 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 produced by
// `git commit --fixup=<hash>` / `--squash=<hash>` and (since Git 2.32)
// `--fixup=amend:<hash>` / `--fixup=reword:<hash>`.
var AutoSquashHeaderRegex = regexp.MustCompile(`^(fixup|squash|amend)!\s+.+`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amend! is only produced by git commit --fixup=amend:<hash> / --fixup=reword:<hash> (Git 2.32+, June 2021). Worth a one‑line comment linking or noting that, since most readers will recognize fixup!/squash! but not amend!. I myself didn't recognize it until I searched online.


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 +68,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.AcceptAutoSquash {
return check
}

check.errors = append(check.errors, errors.Errorf("auto-squash commits are rejected by policy: %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 +142,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]
}
64 changes: 60 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,29 @@ func TestConventionalCommitPolicy(t *testing.T) {
CreateCommit: createInvalidEmptyCommit,
ExpectValid: false,
},
{
Name: "FixupRejected",
CreateCommit: createFixupCommit,
ExpectValid: false,
},
{
Name: "FixupAccepted",
CreateCommit: createFixupCommit,
ExpectValid: true,
Conventional: acceptAutoSquashConventional(),
},
{
Name: "SquashAccepted",
CreateCommit: createSquashCommit,
ExpectValid: true,
Conventional: acceptAutoSquashConventional(),
},
{
Name: "AmendAccepted",
CreateCommit: createAmendCommit,
ExpectValid: true,
Conventional: acceptAutoSquashConventional(),
},
} {
func(test testDesc) {
t.Run(test.Name, func(tt *testing.T) {
Expand All @@ -77,7 +101,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 +387,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 acceptAutoSquashConventional() *Conventional {
return &Conventional{
Types: []string{"type"},
Scopes: []string{"scope", "^valid"},
AcceptAutoSquash: true,
}
}

func initRepo(t *testing.T) error {
_, err := exec.CommandContext(t.Context(), "git", "init").Output()
if err != nil {
Expand Down Expand Up @@ -442,3 +480,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
}