Skip to content

Commit

Permalink
Merge pull request #8 from tab/feature/interactive-edit
Browse files Browse the repository at this point in the history
feat(commit): Interactive commit message editing
  • Loading branch information
tab authored Oct 30, 2024
2 parents bff5022 + 8e8b68d commit a205f01
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 214 deletions.
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

0 comments on commit a205f01

Please sign in to comment.