From 310704a67e7a93f5adde4c82af8bb435439febbc Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 23 Oct 2024 21:50:26 +0300 Subject: [PATCH 1/5] feat(commit): Add support for editing the commit message before committing Users can now edit the commit message using their preferred text editor --- cmd/main_test.go | 94 +++++------ internal/commands/changelog/changelog_test.go | 150 +++++++++--------- internal/commands/commit/commit.go | 55 ++++--- internal/config/config_test.go | 98 ++++++------ internal/errors/errors.go | 29 ++-- internal/git/git.go | 45 +++++- internal/git/git_test.go | 6 +- internal/git/mock_git.go | 8 + 8 files changed, 280 insertions(+), 205 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 7830529..43c3b37 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -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) + }) + } } diff --git a/internal/commands/changelog/changelog_test.go b/internal/commands/changelog/changelog_test.go index d9b9601..a7a0b88 100644 --- a/internal/commands/changelog/changelog_test.go +++ b/internal/commands/changelog/changelog_test.go @@ -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) + }) + } } diff --git a/internal/commands/commit/commit.go b/internal/commands/commit/commit.go index 94020d4..2e30cde 100644 --- a/internal/commands/commit/commit.go +++ b/internal/commands/commit/commit.go @@ -9,6 +9,12 @@ import ( "cmt/internal/utils" ) +const ( + Accept = "y" + Edit = "e" + Cancel = "n" +) + type Command struct { Options commands.GenerateOptions InputReader func() (string, error) @@ -43,26 +49,39 @@ func (c *Command) Generate() error { commitMessage = fmt.Sprintf("%s %s", c.Options.Args[0], commitMessage) } - fmt.Printf("šŸ’¬ Message: %s", commitMessage) - fmt.Print("\n\nAccept? (y/n): ") - - answer, err := c.InputReader() - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } - answer = strings.TrimSpace(strings.ToLower(answer)) + for { + fmt.Printf("šŸ’¬ Message: %s", commitMessage) + fmt.Printf("\n\nAccept, edit, or cancel? (%s/%s/%s): ", Accept, Edit, Cancel) - if answer == "y" { - output, err := c.Options.Client.Commit(c.Options.Ctx, commitMessage) + answer, err := c.InputReader() if err != nil { - errors.HandleCommitError(err) - return err + return fmt.Errorf("error reading input: %w", err) } - fmt.Println("šŸš€ Changes committed:") - fmt.Println(output) - } else { - fmt.Println("āŒ Commit aborted") - } + answer = strings.TrimSpace(strings.ToLower(answer)) + + switch answer { + case Accept: + 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) + case Edit: + editedMessage, err := c.Options.Client.Edit(c.Options.Ctx, commitMessage) + if err != nil { + errors.HandleEditError(err) + return err + } - return nil + fmt.Println("\nšŸ§‘šŸ»ā€šŸ’» Commit message was changed successfully!") + commitMessage = editedMessage + continue + default: + fmt.Println("āŒ Commit aborted") + } + + return nil + } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1d6f0ab..634b40e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,58 +1,58 @@ package config import ( - "os" - "testing" + "os" + "testing" - "cmt/internal/errors" + "cmt/internal/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert" ) func Test_GetAPIToken(t *testing.T) { - type result struct { - error error - token string - } - - tests := []struct { - name string - env string - expected result - }{ - { - name: "API token set", - env: "test-api-token", - expected: result{ - error: nil, - token: "test-api-token", - }, - }, - { - name: "API token not set", - env: "", - expected: result{ - error: errors.ErrAPITokenNotSet, - token: "", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - os.Setenv("OPENAI_API_KEY", tt.env) - - token, err := GetAPIToken() - - if tt.expected.error != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expected.error.Error()) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expected.token, token) - } - - os.Unsetenv("OPENAI_API_KEY") - }) - } + type result struct { + error error + token string + } + + tests := []struct { + name string + env string + expected result + }{ + { + name: "API token set", + env: "test-api-token", + expected: result{ + error: nil, + token: "test-api-token", + }, + }, + { + name: "API token not set", + env: "", + expected: result{ + error: errors.ErrAPITokenNotSet, + token: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("OPENAI_API_KEY", tt.env) + + token, err := GetAPIToken() + + if tt.expected.error != nil { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expected.error.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.token, token) + } + + os.Unsetenv("OPENAI_API_KEY") + }) + } } diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 1893dca..bc537cb 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -6,15 +6,22 @@ import ( ) var ( - ErrAPITokenNotSet = errors.New("API token not set") - ErrNoGitChanges = errors.New("no changes to commit") - ErrNoGitCommits = errors.New("no commits found") - ErrNoResponse = errors.New("no response from GPT") - ErrCommitMessageEmpty = errors.New("commit message cannot be empty") - ErrFailedToParseJSON = errors.New("failed to parse JSON response") - ErrInvalidContext = errors.New("invalid context") - ErrNilClient = errors.New("git client is nil") - ErrNilModel = errors.New("GPT model client is nil") + ErrAPITokenNotSet = errors.New("API token not set") + ErrNoResponse = errors.New("no response from GPT") + ErrFailedToParseJSON = errors.New("failed to parse JSON response") + ErrInvalidContext = errors.New("invalid context") + ErrNilClient = errors.New("git client is nil") + ErrNilModel = errors.New("GPT model client is nil") + ErrFailedToLoadGitDiff = errors.New("failed to load git diff") + ErrFailedToLoadGitLog = errors.New("failed to load git log") + ErrFailedToCommit = errors.New("failed to commit changes") + ErrNoGitChanges = errors.New("no changes to commit") + ErrNoGitCommits = errors.New("no commits found") + ErrCommitMessageEmpty = errors.New("commit message cannot be empty") + ErrFailedToCreateFile = errors.New("failed to create file") + ErrFailedToWriteFile = errors.New("failed to write to file") + ErrFailedToReadFile = errors.New("failed to read file") + ErrFailedToRunEditor = errors.New("error running editor") ) var ( @@ -44,3 +51,7 @@ func HandleCommitError(err error) { fmt.Printf("āŒ Error committing changes: %s\n", err) } } + +func HandleEditError(err error) { + fmt.Printf("āŒ Error editing commit message: %s\n", err) +} diff --git a/internal/git/git.go b/internal/git/git.go index 91fcc5e..561b6e4 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -3,7 +3,7 @@ package git import ( "bytes" "context" - "fmt" + "os" "os/exec" "strings" @@ -17,6 +17,7 @@ type Executor interface { type GitClient interface { Diff(ctx context.Context, opts []string) (string, error) Log(ctx context.Context, opts []string) (string, error) + Edit(ctx context.Context, message string) (string, error) Commit(ctx context.Context, message string) (string, error) } @@ -52,7 +53,7 @@ func (g *Git) Diff(ctx context.Context, opts []string) (string, error) { cmd.Stderr = &out if err := cmd.Run(); err != nil { - return "", fmt.Errorf("git diff error: %w", err) + return "", errors.ErrFailedToLoadGitDiff } result := strings.TrimSpace(out.String()) @@ -74,7 +75,7 @@ func (g *Git) Log(ctx context.Context, opts []string) (string, error) { cmd.Stderr = &out if err := cmd.Run(); err != nil { - return "", fmt.Errorf("git log error: %w", err) + return "", errors.ErrFailedToLoadGitLog } result := strings.TrimSpace(out.String()) @@ -85,6 +86,42 @@ func (g *Git) Log(ctx context.Context, opts []string) (string, error) { return result, nil } +func (g *Git) Edit(ctx context.Context, message string) (string, error) { + tmpFile, err := os.CreateTemp("", "editor") + if err != nil { + return "", errors.ErrFailedToCreateFile + } + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(message) + if err != nil { + return "", errors.ErrFailedToWriteFile + } + tmpFile.Close() + + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vi" + } + + cmd := g.Executor.Run(ctx, editor, tmpFile.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return "", errors.ErrFailedToRunEditor + } + + editedMessageBytes, err := os.ReadFile(tmpFile.Name()) + if err != nil { + return "", errors.ErrFailedToReadFile + } + message = strings.TrimSpace(string(editedMessageBytes)) + + return message, nil +} + func (g *Git) Commit(ctx context.Context, message string) (string, error) { if message == "" { return "", errors.ErrCommitMessageEmpty @@ -97,7 +134,7 @@ func (g *Git) Commit(ctx context.Context, message string) (string, error) { cmd.Stderr = &out if err := cmd.Run(); err != nil { - return "", fmt.Errorf("git commit error: %w", err) + return "", errors.ErrFailedToCommit } result := out.String() diff --git a/internal/git/git_test.go b/internal/git/git_test.go index f6844b5..5b050c8 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -62,7 +62,7 @@ func Test_GitDiff(t *testing.T) { }, expected: result{ output: "", - err: errors.New("git diff error: exit status 1"), + err: errors.New("failed to load git diff"), }, }, } @@ -134,7 +134,7 @@ func TestGit_Log(t *testing.T) { }, expected: result{ output: "", - err: errors.New("git log error: exit status 1"), + err: errors.New("failed to load git log"), }, }, } @@ -207,7 +207,7 @@ func Test_GitCommit(t *testing.T) { }, expected: result{ output: "", - err: errors.New("git commit error: exit status 1"), + err: errors.New("failed to commit changes"), }, }, } diff --git a/internal/git/mock_git.go b/internal/git/mock_git.go index c5c9592..05c78db 100644 --- a/internal/git/mock_git.go +++ b/internal/git/mock_git.go @@ -60,6 +60,14 @@ type MockGitClient struct { recorder *MockGitClientMockRecorder } +func (m *MockGitClient) Edit(ctx context.Context, message string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Edit", ctx, message) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + // MockGitClientMockRecorder is the mock recorder for MockGitClient. type MockGitClientMockRecorder struct { mock *MockGitClient From 36ff8996607fb5aba6004302e9bb86fe8fe19992 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 23 Oct 2024 21:53:01 +0300 Subject: [PATCH 2/5] fix(gpt): Update commit message format guidelines Revised the format guidelines for Conventional Commits to include a more comprehensive list of types and clarify the scope description. --- internal/gpt/gpt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/gpt/gpt.go b/internal/gpt/gpt.go index 17f1000..9de7b59 100644 --- a/internal/gpt/gpt.go +++ b/internal/gpt/gpt.go @@ -57,8 +57,8 @@ Follow these guidelines: Follow the format below: { - "type": "feat", - "scope": "scope of the change", + "type": "feat, fix, build, chore, ci, docs, style, refactor, perf, test, revert", + "scope": "scope of the change (use one word)", "description": "a brief description of what was changed", "body": "an optional longer explanation of the change", "footer": "any additional information, like breaking changes or issue links" From 005ba165caf5fb760b0cc19c9c259cc3a5a1daa5 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 23 Oct 2024 21:57:46 +0300 Subject: [PATCH 3/5] fix(readme): Update README Updated the README to include support for editing commit messages interactively before committing. --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d936fe1..38e7181 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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: @@ -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 From 6ca37868f04e027c45df97eb01942085bb5c87f1 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 23 Oct 2024 22:05:56 +0300 Subject: [PATCH 4/5] fix(commit): Refactor commit message handling logic Modified the flow to allow for accepting the commit message after editing without restarting the loop. --- internal/commands/commit/commit.go | 64 ++++++++++++++++-------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/internal/commands/commit/commit.go b/internal/commands/commit/commit.go index 2e30cde..3415eba 100644 --- a/internal/commands/commit/commit.go +++ b/internal/commands/commit/commit.go @@ -49,39 +49,43 @@ func (c *Command) Generate() error { commitMessage = fmt.Sprintf("%s %s", c.Options.Args[0], commitMessage) } - for { - fmt.Printf("šŸ’¬ Message: %s", commitMessage) - fmt.Printf("\n\nAccept, edit, or cancel? (%s/%s/%s): ", Accept, Edit, Cancel) + fmt.Printf("šŸ’¬ Message: %s", commitMessage) + fmt.Printf("\n\nAccept, edit, or cancel? (%s/%s/%s): ", Accept, Edit, Cancel) - answer, err := c.InputReader() + isAccepted := false + + answer, err := c.InputReader() + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + answer = strings.TrimSpace(strings.ToLower(answer)) + + switch answer { + case Accept: + isAccepted = true + case Edit: + editedMessage, err := c.Options.Client.Edit(c.Options.Ctx, commitMessage) if err != nil { - return fmt.Errorf("error reading input: %w", err) - } - answer = strings.TrimSpace(strings.ToLower(answer)) - - switch answer { - case Accept: - 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) - case Edit: - editedMessage, err := c.Options.Client.Edit(c.Options.Ctx, commitMessage) - if err != nil { - errors.HandleEditError(err) - return err - } - - fmt.Println("\nšŸ§‘šŸ»ā€šŸ’» Commit message was changed successfully!") - commitMessage = editedMessage - continue - default: - fmt.Println("āŒ Commit aborted") + errors.HandleEditError(err) + return err } - return nil + fmt.Println("\nšŸ§‘šŸ»ā€šŸ’» Commit message was changed successfully!") + commitMessage = editedMessage + isAccepted = true + default: + fmt.Println("āŒ Commit aborted") } + + if isAccepted { + 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) + } + + return nil } From 8e8b68db2a68945645e62e0c62ee8e4f3836b864 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 30 Oct 2024 21:37:29 +0200 Subject: [PATCH 5/5] refactor(context): Replace hardcoded timeout with config timeout Make Edit call without timeout context Make Commit call with own timeout context Use a configurable timeout value from the config package --- cmd/main.go | 8 ++------ internal/commands/commit/commit.go | 8 +++++++- internal/commands/commit/commit_test.go | 23 +++++++++++------------ internal/config/config.go | 5 +++++ internal/git/mock_git.go | 23 +++++++++++++++-------- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 7683f5a..0a9a4fe 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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() diff --git a/internal/commands/commit/commit.go b/internal/commands/commit/commit.go index 3415eba..227d078 100644 --- a/internal/commands/commit/commit.go +++ b/internal/commands/commit/commit.go @@ -1,10 +1,12 @@ package commit import ( + "context" "fmt" "strings" "cmt/internal/commands" + "cmt/internal/config" "cmt/internal/errors" "cmt/internal/utils" ) @@ -64,7 +66,7 @@ func (c *Command) Generate() error { case Accept: isAccepted = true case Edit: - editedMessage, err := c.Options.Client.Edit(c.Options.Ctx, commitMessage) + editedMessage, err := c.Options.Client.Edit(context.Background(), commitMessage) if err != nil { errors.HandleEditError(err) return err @@ -78,6 +80,10 @@ func (c *Command) Generate() error { } 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) diff --git a/internal/commands/commit/commit_test.go b/internal/commands/commit/commit_test.go index 730b22d..d9b47d5 100644 --- a/internal/commands/commit/commit_test.go +++ b/internal/commands/commit/commit_test.go @@ -2,7 +2,6 @@ package commit import ( "bytes" - "cmt/internal/commands" "context" "fmt" "io" @@ -12,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "cmt/internal/commands" "cmt/internal/git" "cmt/internal/gpt" ) @@ -22,10 +22,9 @@ func Test_Generate(t *testing.T) { mockGitClient := git.NewMockGitClient(ctrl) mockGPTModelClient := gpt.NewMockGPTModelClient(ctrl) - ctx := context.Background() options := commands.GenerateOptions{ - Ctx: ctx, + Ctx: context.Background(), Client: mockGitClient, Model: mockGPTModelClient, } @@ -47,9 +46,9 @@ func Test_Generate(t *testing.T) { return "y", nil }, before: func() { - mockGitClient.EXPECT().Diff(ctx, nil).Return("mock diff output", nil) - mockGPTModelClient.EXPECT().FetchCommitMessage(ctx, "mock diff output").Return("feat(core): Description", nil) - mockGitClient.EXPECT().Commit(ctx, "feat(core): Description").Return("[feature/core 29ca12d] feat(core): Description", nil) + mockGitClient.EXPECT().Diff(gomock.Any(), nil).Return("mock diff output", nil) + mockGPTModelClient.EXPECT().FetchCommitMessage(gomock.Any(), "mock diff output").Return("feat(core): Description", nil) + mockGitClient.EXPECT().Commit(gomock.Any(), "feat(core): Description").Return("[feature/core 29ca12d] feat(core): Description", nil) }, expected: result{ output: "šŸš€ Changes committed:\n[feature/core 29ca12d] feat(core): Description", @@ -62,8 +61,8 @@ func Test_Generate(t *testing.T) { return "n", nil }, before: func() { - mockGitClient.EXPECT().Diff(ctx, nil).Return("mock diff output", nil) - mockGPTModelClient.EXPECT().FetchCommitMessage(ctx, "mock diff output").Return("feat(core): Description", nil) + mockGitClient.EXPECT().Diff(gomock.Any(), nil).Return("mock diff output", nil) + mockGPTModelClient.EXPECT().FetchCommitMessage(gomock.Any(), "mock diff output").Return("feat(core): Description", nil) }, expected: result{ output: "āŒ Commit aborted", @@ -76,7 +75,7 @@ func Test_Generate(t *testing.T) { return "", nil }, before: func() { - mockGitClient.EXPECT().Diff(ctx, nil).Return("", fmt.Errorf("git diff error")) + mockGitClient.EXPECT().Diff(gomock.Any(), nil).Return("", fmt.Errorf("git diff error")) }, expected: result{ output: "āŒ Error getting git diff: git diff error", @@ -89,9 +88,9 @@ func Test_Generate(t *testing.T) { return "y", nil }, before: func() { - mockGitClient.EXPECT().Diff(ctx, nil).Return("mock diff output", nil) - mockGPTModelClient.EXPECT().FetchCommitMessage(ctx, "mock diff output").Return("feat(core): Description", nil) - mockGitClient.EXPECT().Commit(ctx, "feat(core): Description").Return("", fmt.Errorf("git commit error")) + mockGitClient.EXPECT().Diff(gomock.Any(), nil).Return("mock diff output", nil) + mockGPTModelClient.EXPECT().FetchCommitMessage(gomock.Any(), "mock diff output").Return("feat(core): Description", nil) + mockGitClient.EXPECT().Commit(gomock.Any(), "feat(core): Description").Return("", fmt.Errorf("git commit error")) }, expected: result{ output: "āŒ Error committing changes: git commit error", diff --git a/internal/config/config.go b/internal/config/config.go index cf49317..788febe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,10 +3,15 @@ package config import ( "fmt" "os" + "time" "cmt/internal/errors" ) +const ( + Timeout = 60 * time.Second +) + func GetAPIToken() (string, error) { token := os.Getenv("OPENAI_API_KEY") if token == "" { diff --git a/internal/git/mock_git.go b/internal/git/mock_git.go index 05c78db..0e4bb99 100644 --- a/internal/git/mock_git.go +++ b/internal/git/mock_git.go @@ -60,14 +60,6 @@ type MockGitClient struct { recorder *MockGitClientMockRecorder } -func (m *MockGitClient) Edit(ctx context.Context, message string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Edit", ctx, message) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - // MockGitClientMockRecorder is the mock recorder for MockGitClient. type MockGitClientMockRecorder struct { mock *MockGitClient @@ -85,6 +77,21 @@ func (m *MockGitClient) EXPECT() *MockGitClientMockRecorder { return m.recorder } +// Edit mocks base method. +func (m *MockGitClient) Edit(ctx context.Context, message string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Edit", ctx, message) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Edit indicates an expected call of Edit. +func (mr *MockGitClientMockRecorder) Edit(ctx, message interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Edit", reflect.TypeOf((*MockGitClient)(nil).Edit), ctx, message) +} + // Commit mocks base method. func (m *MockGitClient) Commit(ctx context.Context, message string) (string, error) { m.ctrl.T.Helper()