Skip to content

Commit

Permalink
feat: add multi commit support
Browse files Browse the repository at this point in the history
Add the ability to enforce commit policies on a commit range.

Two options are available:
- --revision-range <commit1..commit2>: analyze the given git revision
  range
- --base-branch <name>: convenience for --revision-range name..HEAD

If commit1 is not an ancestor of commit2, the merge base (first common
ancestor) is used as first commit.

Signed-off-by: Vincent Dupont <[email protected]>
Signed-off-by: Noel Georgi <[email protected]>
  • Loading branch information
vincent-d authored and frezbo committed Sep 19, 2022
1 parent 1130050 commit 9023e3a
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 25 deletions.
3 changes: 1 addition & 2 deletions .conform.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-09-19T13:49:50Z by kres 9ea8a33.
# Generated on 2022-09-19T19:55:11Z by kres 255fc05.

---
policies:
Expand Down Expand Up @@ -29,7 +29,6 @@ policies:
skipPaths:
- .git/
- testdata/
- internal/policy/license/
includeSuffixes:
- .go
excludeSuffixes:
Expand Down
5 changes: 4 additions & 1 deletion .drone.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2022-09-19T13:49:50Z by kres 9ea8a33.
# Generated on 2022-09-19T19:52:38Z by kres 255fc05.

kind: pipeline
type: kubernetes
Expand Down Expand Up @@ -340,6 +340,9 @@ trigger:
exclude:
- renovate/*
- dependabot/*
status:
- success
- failure

depends_on:
- default
Expand Down
8 changes: 8 additions & 0 deletions cmd/conform/enforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ var enforceCmd = &cobra.Command{
opts = append(opts, policy.WithCommitRef(commitRef))
}

if baseBranch := cmd.Flags().Lookup("base-branch").Value.String(); baseBranch != "" {
opts = append(opts, policy.WithRevisionRange(fmt.Sprintf("%s..HEAD", baseBranch)))
} else if revisionRange := cmd.Flags().Lookup("revision-range").Value.String(); revisionRange != "" {
opts = append(opts, policy.WithRevisionRange(revisionRange))
}

return e.Enforce(opts...)
},
}
Expand All @@ -51,5 +57,7 @@ func init() {
enforceCmd.Flags().String("commit-msg-file", "", "the path to the temporary commit message file")
enforceCmd.Flags().String("commit-ref", "", "the ref to compare git policies against")
enforceCmd.Flags().String("reporter", "none", "the reporter method to use")
enforceCmd.Flags().String("revision-range", "", "<commit1>..<commit2>")
enforceCmd.Flags().String("base-branch", "", "base branch to compare with")
rootCmd.AddCommand(enforceCmd)
}
50 changes: 50 additions & 0 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/keybase/go-crypto/openpgp"
"github.com/pkg/errors"
)

// Git is a helper for git.
Expand Down Expand Up @@ -85,6 +86,55 @@ func (g *Git) Message() (message string, err error) {
return message, err
}

// Messages returns the list of commit messages in the range commit1..commit2.
func (g *Git) Messages(commit1, commit2 string) ([]string, error) {
hash1, err := g.repo.ResolveRevision(plumbing.Revision(commit1))
if err != nil {
return nil, err
}

hash2, err := g.repo.ResolveRevision(plumbing.Revision(commit2))
if err != nil {
return nil, err
}

c2, err := g.repo.CommitObject(*hash2)
if err != nil {
return nil, err
}

c1, err := g.repo.CommitObject(*hash1)
if err != nil {
return nil, err
}

if ok, ancestorErr := c1.IsAncestor(c2); ancestorErr != nil || !ok {
c, mergeBaseErr := c1.MergeBase(c2)
if mergeBaseErr != nil {
return nil, errors.Errorf("invalid ancestor %s", c1)
}

c1 = c[0]
}

msgs := make([]string, 0)

for {
msgs = append(msgs, c2.Message)

c2, err = c2.Parents().Next()
if err != nil {
return nil, err
}

if c2.ID() == c1.ID() {
break
}
}

return msgs, nil
}

// HasGPGSignature returns the commit message. In the case that a commit has multiple
// parents, the message of the last parent is returned.
//
Expand Down
51 changes: 43 additions & 8 deletions internal/policy/commit/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,47 @@ func (c *Commit) Compliance(options *policy.Options) (*policy.Report, error) {
return report, errors.Errorf("failed to open git repo: %v", err)
}

var msg string
var msgs []string

if options.CommitMsgFile != nil {
switch o := options; {
case o.CommitMsgFile != nil:
var contents []byte

if contents, err = os.ReadFile(*options.CommitMsgFile); err != nil {
return report, errors.Errorf("failed to read commit message file: %v", err)
}

msg = string(contents)
} else if msg, err = g.Message(); err != nil {
return report, errors.Errorf("failed to get commit message: %v", err)
msgs = append(msgs, string(contents))
case o.RevisionRange != "":
revs, err := extractRevisionRange(options)
if err != nil {
return report, errors.Errorf("failed to get commit message: %v", err)
}

msgs, err = g.Messages(revs[0], revs[1])
if err != nil {
return report, errors.Errorf("failed to get commit message: %v", err)
}
default:
msg, err := g.Message()
if err != nil {
return report, errors.Errorf("failed to get commit message: %v", err)
}

msgs = append(msgs, msg)
}

for i := range msgs {
c.msg = msgs[i]

c.compliance(report, g, options)
}

c.msg = msg
return report, nil
}

// compliance checks the compliance with the policies of the given commit.
func (c *Commit) compliance(report *policy.Report, g *git.Git, options *policy.Options) {
if c.Header != nil {
if c.Header.Length != 0 {
report.AddCheck(c.ValidateHeaderLength())
Expand Down Expand Up @@ -171,8 +196,6 @@ func (c *Commit) Compliance(options *policy.Options) (*policy.Report, error) {
report.AddCheck(c.ValidateBody())
}
}

return report, nil
}

func (c Commit) firstWord() (string, error) {
Expand Down Expand Up @@ -206,3 +229,15 @@ func (c Commit) firstWord() (string, error) {
func (c Commit) header() string {
return strings.Split(strings.TrimPrefix(c.msg, "\n"), "\n")[0]
}

func extractRevisionRange(options *policy.Options) ([]string, error) {
revs := strings.Split(options.RevisionRange, "..")
if len(revs) > 2 || len(revs) == 0 || revs[0] == "" || revs[1] == "" {
return nil, errors.New("invalid revision range")
} else if len(revs) == 1 {
// if no final rev is given, use HEAD as default
revs = append(revs, "HEAD")
}

return revs, nil
}
111 changes: 102 additions & 9 deletions internal/policy/commit/commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@
package commit

import (
"log"
"fmt"
"os"
"os/exec"
"strings"
"testing"

"github.com/siderolabs/conform/internal/policy"
)

func RemoveAll(dir string) {
if err := os.RemoveAll(dir); err != nil {
log.Fatal(err)
}
}

//nolint:gocognit
func TestConventionalCommitPolicy(t *testing.T) {
//nolint:govet
Expand Down Expand Up @@ -261,8 +256,6 @@ func TestValidConventionalCommitPolicyRegex(t *testing.T) {
func TestInvalidConventionalCommitPolicyRegex(t *testing.T) {
dir := t.TempDir()

defer RemoveAll(dir)

err := os.Chdir(dir)
if err != nil {
t.Error(err)
Expand All @@ -288,6 +281,106 @@ func TestInvalidConventionalCommitPolicyRegex(t *testing.T) {
}
}

func TestValidRevisionRange(t *testing.T) {
dir := t.TempDir()

err := os.Chdir(dir)
if err != nil {
t.Error(err)
}

err = initRepo()
if err != nil {
t.Error(err)
}

revs, err := createValidCommitRange()
if err != nil {
t.Fatal(err)
}

// Test with a valid revision range
report, err := runComplianceRange(revs[0], revs[len(revs)-1])
if err != nil {
t.Error(err)
}

if !report.Valid() {
t.Error("Report is invalid with valid conventional commits")
}

// Test with HEAD as end of revision range
report, err = runComplianceRange(revs[0], "HEAD")
if err != nil {
t.Error(err)
}

if !report.Valid() {
t.Error("Report is invalid with valid conventional commits")
}

// Test with empty end of revision range (should fail)
_, err = runComplianceRange(revs[0], "")
if err == nil {
t.Error("Invalid end of revision, got success, expecting failure")
}

// Test with empty start of revision (should fail)
_, err = runComplianceRange("", "HEAD")
if err == nil {
t.Error("Invalid end of revision, got success, expecting failure")
}

// Test with start of revision not an ancestor of end of range (should fail)
_, err = runComplianceRange(revs[1], revs[0])
if err == nil {
t.Error("Invalid end of revision, got success, expecting failure")
}
}

func createValidCommitRange() ([]string, error) {
var revs []string

for i := 0; i < 4; i++ {
err := os.WriteFile("test", []byte(fmt.Sprint(i)), 0o644)
if err != nil {
return nil, fmt.Errorf("writing test file failed: %w", err)
}

_, err = exec.Command("git", "add", "test").Output()
if err != nil {
return nil, fmt.Errorf("git add failed: %w", err)
}

_, err = exec.Command("git", "-c", "user.name='test'", "-c", "user.email='[email protected]'", "commit", "-m", fmt.Sprintf("type(scope): description %d", i)).Output()
if err != nil {
return nil, fmt.Errorf("git commit failed: %w", err)
}

id, err := exec.Command("git", "rev-parse", "HEAD").Output()
if err != nil {
return nil, fmt.Errorf("rev-parse failed: %w", err)
}

revs = append(revs, strings.TrimSpace(string(id)))
}

return revs, nil
}

func runComplianceRange(id1, id2 string) (*policy.Report, error) {
c := &Commit{
Conventional: &Conventional{
Types: []string{"type"},
Scopes: []string{"scope", "^valid"},
},
}

return c.Compliance(&policy.Options{
RevisionRange: fmt.Sprintf("%s..%s", id1, id2),
})
}

func runCompliance() (*policy.Report, error) {
c := &Commit{
Conventional: &Conventional{
Expand Down
10 changes: 5 additions & 5 deletions internal/policy/license/license_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//go:build !some_test_tag
// +build !some_test_tag

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//go:build !some_test_tag
// +build !some_test_tag

package license_test

import (
Expand All @@ -23,7 +23,7 @@ func TestLicense(t *testing.T) {

t.Run("Default", func(t *testing.T) {
l := license.License{
IncludeSuffixes: []string{".go"},
IncludeSuffixes: []string{".txt"},
AllowPrecedingComments: false,
Header: header,
}
Expand All @@ -33,7 +33,7 @@ func TestLicense(t *testing.T) {

t.Run("AllowPrecedingComments", func(t *testing.T) {
l := license.License{
IncludeSuffixes: []string{".go"},
IncludeSuffixes: []string{".txt"},
AllowPrecedingComments: true,
Header: header,
}
Expand Down
5 changes: 5 additions & 0 deletions internal/policy/license/testdata/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//this is a preceding comment

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 changes: 8 additions & 0 deletions internal/policy/policy_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Option func(*Options)
type Options struct {
CommitMsgFile *string
CommitRef string
RevisionRange string
}

// WithCommitMsgFile sets the path to the commit message file.
Expand All @@ -27,6 +28,13 @@ func WithCommitRef(o string) Option {
}
}

// WithRevisionRange sets the revision range to compare git policies against.
func WithRevisionRange(o string) Option {
return func(args *Options) {
args.RevisionRange = o
}
}

// NewDefaultOptions initializes a Options struct with default values.
func NewDefaultOptions(setters ...Option) *Options {
opts := &Options{
Expand Down

0 comments on commit 9023e3a

Please sign in to comment.