Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commit): Interactive commit message editing #8

Merged
merged 5 commits into from
Oct 30, 2024
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ It automates the process of writing clear and structured commit messages, enhanc

- **Automated Commit Messages**: Generates commit messages following the [Conventional Commits](https://www.conventionalcommits.org/) specification.
- **Interactive Approval**: Allows you to review and approve the generated commit message before committing.
- **Interactive Edit**: Supports editing the commit message interactively before committing.
- **Custom Prefixes**: Supports adding custom prefixes to commit messages for better traceability (e.g., task IDs, issue numbers).
- **Changelog Generation**: Automatically creates changelogs based on your commit history.
- **Integration with OpenAI GPT**: Utilizes GPT to analyze your staged changes and produce meaningful commit messages.
Expand Down Expand Up @@ -88,10 +89,10 @@ Review the generated commit message and choose whether to commit or not.

Implemented JWT-based authentication for API endpoints. Users can now log in and receive a token for subsequent requests.

Accept? (y/n):
Accept, edit, or cancel? (y/e/n):
```

Type **y** to accept and commit the changes, or **n** to abort.
Type **y** to accept and commit the changes, **e** to edit message or **n** to abort.

```sh
🚀 Changes committed:
Expand All @@ -115,7 +116,7 @@ Resulting commit message:

Implemented JWT-based authentication for API endpoints. Users can now log in and receive a token for subsequent requests.

Accept? (y/n):
Accept, edit, or cancel? (y/e/n):
```

### Changelog generation
Expand Down
8 changes: 2 additions & 6 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,18 @@ import (
"log"
"os"
"strings"
"time"

"cmt/internal/cli"
"cmt/internal/commands"
"cmt/internal/commands/changelog"
"cmt/internal/commands/commit"
"cmt/internal/config"
"cmt/internal/git"
"cmt/internal/gpt"
)

const (
Timeout = 60 * time.Second
)

func main() {
ctx, cancel := context.WithTimeout(context.Background(), Timeout)
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()

client := git.NewGitClient()
Expand Down
94 changes: 47 additions & 47 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
package main

import (
"bytes"
"io"
"os"
"testing"
"bytes"
"io"
"os"
"testing"

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

func TestMainRun(t *testing.T) {
origArgs := os.Args
defer func() { os.Args = origArgs }()

tests := []struct {
name string
args []string
output string
}{
{
name: "Help command",
args: []string{"--help"},
output: "Usage:",
},
{
name: "Version command",
args: []string{"--version"},
output: "cmt 0.3.0\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Args = append([]string{"cmd"}, tt.args...)

r, w, _ := os.Pipe()
origStdout := os.Stdout
os.Stdout = w
defer func() { os.Stdout = origStdout }()

main()

w.Close()
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
return
}
result := buf.String()

assert.Contains(t, result, tt.output)
})
}
origArgs := os.Args
defer func() { os.Args = origArgs }()

tests := []struct {
name string
args []string
output string
}{
{
name: "Help command",
args: []string{"--help"},
output: "Usage:",
},
{
name: "Version command",
args: []string{"--version"},
output: "cmt 0.3.0\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Args = append([]string{"cmd"}, tt.args...)

r, w, _ := os.Pipe()
origStdout := os.Stdout
os.Stdout = w
defer func() { os.Stdout = origStdout }()

main()

w.Close()
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
return
}
result := buf.String()

assert.Contains(t, result, tt.output)
})
}
}
150 changes: 75 additions & 75 deletions internal/commands/changelog/changelog_test.go
Original file line number Diff line number Diff line change
@@ -1,93 +1,93 @@
package changelog

import (
"bytes"
"cmt/internal/commands"
"context"
"fmt"
"io"
"os"
"testing"
"bytes"
"cmt/internal/commands"
"context"
"fmt"
"io"
"os"
"testing"

"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"

"cmt/internal/git"
"cmt/internal/gpt"
"cmt/internal/git"
"cmt/internal/gpt"
)

func Test_Generate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockGitClient := git.NewMockGitClient(ctrl)
mockGPTModelClient := gpt.NewMockGPTModelClient(ctrl)
ctx := context.Background()

mockGitClient := git.NewMockGitClient(ctrl)
mockGPTModelClient := gpt.NewMockGPTModelClient(ctrl)
ctx := context.Background()
options := commands.GenerateOptions{
Ctx: ctx,
Client: mockGitClient,
Model: mockGPTModelClient,
}

options := commands.GenerateOptions{
Ctx: ctx,
Client: mockGitClient,
Model: mockGPTModelClient,
}
type result struct {
output string
err bool
}

type result struct {
output string
err bool
}
tests := []struct {
name string
before func()
expected result
}{
{
name: "Success",
before: func() {
mockGitClient.EXPECT().Log(ctx, nil).Return("mock log output", nil)
mockGPTModelClient.EXPECT().FetchChangelog(ctx, "mock log output").Return("# CHANGELOG", nil)
},
expected: result{
output: "💬 Changelog: \n\n# CHANGELOG",
err: false,
},
},
{
name: "Error fetching log",
before: func() {
mockGitClient.EXPECT().Log(ctx, nil).Return("", fmt.Errorf("git log error"))
},
expected: result{
output: "",
err: true,
},
},
}

tests := []struct {
name string
before func()
expected result
}{
{
name: "Success",
before: func() {
mockGitClient.EXPECT().Log(ctx, nil).Return("mock log output", nil)
mockGPTModelClient.EXPECT().FetchChangelog(ctx, "mock log output").Return("# CHANGELOG", nil)
},
expected: result{
output: "💬 Changelog: \n\n# CHANGELOG",
err: false,
},
},
{
name: "Error fetching log",
before: func() {
mockGitClient.EXPECT().Log(ctx, nil).Return("", fmt.Errorf("git log error"))
},
expected: result{
output: "",
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.before()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.before()
r, w, _ := os.Pipe()
defer r.Close()
defer w.Close()
origStdout := os.Stdout
os.Stdout = w
defer func() { os.Stdout = origStdout }()

r, w, _ := os.Pipe()
defer r.Close()
defer w.Close()
origStdout := os.Stdout
os.Stdout = w
defer func() { os.Stdout = origStdout }()
err := NewCommand(options).Generate()

err := NewCommand(options).Generate()
w.Close()
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()

w.Close()
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if tt.expected.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}

if tt.expected.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}

assert.Contains(t, output, tt.expected.output)
})
}
assert.Contains(t, output, tt.expected.output)
})
}
}
37 changes: 33 additions & 4 deletions internal/commands/commit/commit.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package commit

import (
"context"
"fmt"
"strings"

"cmt/internal/commands"
"cmt/internal/config"
"cmt/internal/errors"
"cmt/internal/utils"
)

const (
Accept = "y"
Edit = "e"
Cancel = "n"
)

type Command struct {
Options commands.GenerateOptions
InputReader func() (string, error)
Expand Down Expand Up @@ -44,24 +52,45 @@ func (c *Command) Generate() error {
}

fmt.Printf("💬 Message: %s", commitMessage)
fmt.Print("\n\nAccept? (y/n): ")
fmt.Printf("\n\nAccept, edit, or cancel? (%s/%s/%s): ", Accept, Edit, Cancel)

isAccepted := false

answer, err := c.InputReader()
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
answer = strings.TrimSpace(strings.ToLower(answer))

if answer == "y" {
switch answer {
case Accept:
isAccepted = true
case Edit:
editedMessage, err := c.Options.Client.Edit(context.Background(), commitMessage)
if err != nil {
errors.HandleEditError(err)
return err
}

fmt.Println("\n🧑🏻‍💻 Commit message was changed successfully!")
commitMessage = editedMessage
isAccepted = true
default:
fmt.Println("❌ Commit aborted")
}

if isAccepted {
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
c.Options.Ctx = ctx
defer cancel()

output, err := c.Options.Client.Commit(c.Options.Ctx, commitMessage)
if err != nil {
errors.HandleCommitError(err)
return err
}
fmt.Println("🚀 Changes committed:")
fmt.Println(output)
} else {
fmt.Println("❌ Commit aborted")
}

return nil
Expand Down
Loading