Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
reviews:
profile: chill
request_changes_workflow: true
high_level_summary: true
review_status: true
commit_status: true
collapse_walkthrough: true
auto_assign_reviewers: true
in_progress_fortune: false
poem: false
auto_review:
enabled: true
auto_incremental_review: true
ignore_title_keywords:
- "WIP"
- "DO NOT MERGE"
drafts: false
base_branches:
- "main"
pre_merge_checks:
title:
mode: error
requirements: 'Title should be concise and descriptive, ideally under 72 characters. Use imperative mood (e.g., "Add database for snippets").'
chat:
auto_reply: true
17 changes: 8 additions & 9 deletions .lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,15 @@ pre-commit:
- go.sum
stage_fixed: true
- name: lint & format
run: golangci-lint run ./...
glob: "*.go"
stage_fixed: true
- name: format markdown
glob: "*md"
run: go tool markdownfmt -w .
stage_fixed: true
- name: check release config
glob: ".goreleaser.yaml"
run: go tool goreleaser check
group:
parallel: false
jobs:
- name: format
run: golangci-lint fmt ./...
stage_fixed: true
- name: lint
run: golangci-lint run ./...

post-checkout:
parallel: false
Expand Down
1 change: 1 addition & 0 deletions cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import (

var commands = []*cli.Command{
version(),
commit(),
}
83 changes: 83 additions & 0 deletions cmd/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cmd

import (
"context"
"errors"
"fmt"

"github.com/urfave/cli/v3"

"github.com/isokolovskii/commitizen/internal/conventional"
)

var ErrInvalidFlag = errors.New("invalid flag")

func commit() *cli.Command {
commitData := conventional.Commit{}

return &cli.Command{
Name: "commit",
Usage: "Create Conventional Commit",
Flags: flags(&commitData),
Action: func(_ context.Context, _ *cli.Command) error {
message, err := conventional.BuildCommitMessage(&commitData)
if err != nil {
return fmt.Errorf("error building commit: %w", err)
}

_, err = fmt.Println(message)
if err != nil {
return fmt.Errorf("error printing built commit message: %w", err)
}

return nil
},
}
}

func flags(commitData *conventional.Commit) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "type",
OnlyOnce: true,
Destination: &commitData.Type,
Required: true,
Usage: "Type of change (e.g., feat, fix, docs)",
},
&cli.StringFlag{
Name: "scope",
OnlyOnce: true,
Destination: &commitData.Scope,
Required: false,
Usage: "Optional context for the change (e.g., api, cli)",
},
&cli.StringFlag{
Name: "title",
OnlyOnce: true,
Destination: &commitData.Title,
Required: true,
Usage: "Short description of changes",
},
&cli.StringFlag{
Name: "body",
OnlyOnce: true,
Destination: &commitData.Body,
Required: false,
Usage: "Optional longer description of the change",
},
&cli.StringFlag{
Name: "breaking",
OnlyOnce: true,
Destination: &commitData.BreakingChange,
Required: false,
Usage: "Optional description of breaking changes introduced with commit",
},
&cli.StringFlag{
Name: "issue",
OnlyOnce: true,
Destination: &commitData.Issue,
Required: false,
Usage: "Optional issue number",
},
}
}
20 changes: 20 additions & 0 deletions internal/conventional/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package conventional

type (
// Commit represents the components of a Conventional Commit message.
// See https://www.conventionalcommits.org/ for the specification.
Commit struct {
// Type describes the kind of change (e.g., "feat", "fix", "docs").
Type string
// Scope is an optional context for the change (e.g., "api", "cli").
Scope string
// Title is a short description of the change.
Title string
// Body is an optional longer description.
Body string
// Breaking change description.
BreakingChange string
// Issue number.
Issue string
}
)
69 changes: 69 additions & 0 deletions internal/conventional/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package conventional

import (
"errors"
"fmt"
)

const (
newLine = "\n"
)

// ErrRequiredPartNotPreset - error used for missing required conventional commit fields.
var ErrRequiredPartNotPreset = errors.New("required part not present")

// BuildCommitMessage - builds conventional commit message.
func BuildCommitMessage(commit *Commit) (string, error) {
if commit.Type == "" {
return "", fmt.Errorf("%w: type", ErrRequiredPartNotPreset)
}

if commit.Title == "" {
return "", fmt.Errorf("%w: title", ErrRequiredPartNotPreset)
}

header := buildHeader(commit)
footer := buildFooter(commit)

return header + newLine + footer, nil
}

func buildHeader(commit *Commit) string {
header := commit.Type

if commit.Scope != "" {
header = fmt.Sprintf("%s(%s)", header, commit.Scope)
}

if commit.BreakingChange != "" {
header += "!"
}

header += ": " + commit.Title

return header
}

func buildFooter(commit *Commit) string {
footer := ""

if commit.BreakingChange != "" {
footer += newLine + "BREAKING CHANGE: " + commit.BreakingChange
}

if commit.Issue != "" {
footer += newLine + "Issue: " + commit.Issue
}

if commit.Body != "" {
if footer != "" {
footer = newLine + commit.Body + newLine + footer + newLine
} else {
footer = newLine + commit.Body + newLine
}
} else if footer != "" {
footer += newLine
}

return footer
}
110 changes: 110 additions & 0 deletions internal/conventional/message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package conventional

import (
"strconv"
"testing"

"github.com/stretchr/testify/assert"
)

type (
testCases struct {
commitData *Commit
err error
result string
}
)

var buildCommitMessageTestCases = []testCases{
{
result: "feat(api): some changes\n\nChanges description\n",
commitData: &Commit{
Title: "some changes",
Type: "feat",
Scope: "api",
Body: "Changes description",
},
err: nil,
},
{
result: "feat(api): some changes\n",
commitData: &Commit{
Title: "some changes",
Type: "feat",
Scope: "api",
},
err: nil,
},
{
result: "feat: some changes\n\nChanges description\n",
commitData: &Commit{
Title: "some changes",
Type: "feat",
Body: "Changes description",
},
err: nil,
},
{
result: "feat(api): some changes\n\nChanges description\n\nIssue: TEST-1\n",
commitData: &Commit{
Title: "some changes",
Type: "feat",
Scope: "api",
Body: "Changes description",
Issue: "TEST-1",
},
err: nil,
},
{
result: "feat(api)!: some changes\n\nChanges description\n\nBREAKING CHANGE: Something breaking\nIssue: TEST-1\n",
commitData: &Commit{
Title: "some changes",
Type: "feat",
Scope: "api",
Body: "Changes description",
Issue: "TEST-1",
BreakingChange: "Something breaking",
},
err: nil,
},
{
result: "feat!: some changes\n\nChanges description\n\nBREAKING CHANGE: Something breaking\n",
commitData: &Commit{
Title: "some changes",
Type: "feat",
Body: "Changes description",
BreakingChange: "Something breaking",
},
err: nil,
},
{
result: "",
commitData: &Commit{
Type: "feat",
Body: "Changes description",
},
err: ErrRequiredPartNotPreset,
},
{
result: "",
commitData: &Commit{
Title: "some changes",
Body: "Changes description",
},
err: ErrRequiredPartNotPreset,
},
}

func TestBuildCommitMessage(t *testing.T) {
t.Parallel()
for i, tt := range buildCommitMessageTestCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
t.Parallel()

message, err := BuildCommitMessage(tt.commitData)

assert.Equal(t, tt.result, message)
assert.ErrorIs(t, err, tt.err)
})
}
}
Loading