Skip to content

Commit bc111f3

Browse files
committed
feat: Validate running inside git work tree
Ensure the application only runs if executed within a Git work tree. This prevents cryptic errors when attempting Git operations in an incorrect environment, failing fast on startup.
1 parent 22e5ccf commit bc111f3

File tree

3 files changed

+67
-11
lines changed

3 files changed

+67
-11
lines changed

internal/app/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
var ErrAborted = errors.New("aborted")
1717

1818
type GitClient interface {
19+
IsInWorkTree() error
1920
Diff() (string, error)
2021
Commit(message string) error
2122
}
@@ -50,6 +51,11 @@ func NewApp(provider llmprovider.Provider, git GitClient, ui UIClient, prompt st
5051
}
5152

5253
func (app *App) Run(ctx context.Context, userMessage string) error {
54+
55+
if err := app.git.IsInWorkTree(); err != nil {
56+
return err
57+
}
58+
5359
app.ui.StartSpinner(" <magenta>Running git diff</>")
5460
defer app.ui.StopSpinner()
5561

internal/app/app_test.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ func (m *mockProvider) Model() string {
2121
}
2222

2323
type mockGitClient struct {
24-
DiffFunc func() (string, error)
25-
CommitFunc func(message string) error
24+
IsInWorkTreeFunc func() error
25+
DiffFunc func() (string, error)
26+
CommitFunc func(message string) error
27+
}
28+
29+
func (m *mockGitClient) IsInWorkTree() error {
30+
return m.IsInWorkTreeFunc()
2631
}
2732

2833
func (m *mockGitClient) Diff() (string, error) {
@@ -79,10 +84,27 @@ func TestNewApp(t *testing.T) {
7984
func TestAppRun(t *testing.T) {
8085
ctx := context.Background()
8186

87+
t.Run("NotInWorkTree", func(t *testing.T) {
88+
mp := &mockProvider{modelName: "test-model"}
89+
gitClient := &mockGitClient{
90+
IsInWorkTreeFunc: func() error { return assert.AnError },
91+
}
92+
uiClient := &mockUIClient{
93+
StartSpinnerFunc: func(message string) {},
94+
UpdateSpinnerFunc: func(message string) {},
95+
StopSpinnerFunc: func() {},
96+
}
97+
app := NewApp(mp, gitClient, uiClient, "prompt")
98+
err := app.Run(ctx, "")
99+
assert.Error(t, err)
100+
assert.Equal(t, assert.AnError, err)
101+
})
102+
82103
t.Run("DiffError", func(t *testing.T) {
83104
mp := &mockProvider{modelName: "test-model"}
84105
gitClient := &mockGitClient{
85-
DiffFunc: func() (string, error) { return "", assert.AnError },
106+
IsInWorkTreeFunc: func() error { return nil },
107+
DiffFunc: func() (string, error) { return "", assert.AnError },
86108
}
87109
uiClient := &mockUIClient{
88110
StartSpinnerFunc: func(message string) {},
@@ -98,7 +120,8 @@ func TestAppRun(t *testing.T) {
98120
t.Run("NoStagedChanges", func(t *testing.T) {
99121
mp := &mockProvider{modelName: "test-model"}
100122
gitClient := &mockGitClient{
101-
DiffFunc: func() (string, error) { return "", nil },
123+
IsInWorkTreeFunc: func() error { return nil },
124+
DiffFunc: func() (string, error) { return "", nil },
102125
}
103126
uiClient := &mockUIClient{
104127
StartSpinnerFunc: func(message string) {},
@@ -117,7 +140,8 @@ func TestAppRun(t *testing.T) {
117140
callFunc: func(ctx context.Context, systemPrompt, userPrompt string) (string, error) { return "", assert.AnError },
118141
}
119142
gitClient := &mockGitClient{
120-
DiffFunc: func() (string, error) { return "diff output", nil },
143+
IsInWorkTreeFunc: func() error { return nil },
144+
DiffFunc: func() (string, error) { return "diff output", nil },
121145
}
122146
uiClient := &mockUIClient{
123147
StartSpinnerFunc: func(message string) {},
@@ -136,7 +160,8 @@ func TestAppRun(t *testing.T) {
136160
callFunc: func(ctx context.Context, systemPrompt, userPrompt string) (string, error) { return "llm message", nil },
137161
}
138162
gitClient := &mockGitClient{
139-
DiffFunc: func() (string, error) { return "diff output", nil },
163+
IsInWorkTreeFunc: func() error { return nil },
164+
DiffFunc: func() (string, error) { return "diff output", nil },
140165
}
141166
uiClient := &mockUIClient{
142167
TextAreaFunc: func(value string) (string, bool, error) { return "", false, assert.AnError },
@@ -156,7 +181,8 @@ func TestAppRun(t *testing.T) {
156181
callFunc: func(ctx context.Context, systemPrompt, userPrompt string) (string, error) { return "llm message", nil },
157182
}
158183
gitClient := &mockGitClient{
159-
DiffFunc: func() (string, error) { return "diff output", nil },
184+
IsInWorkTreeFunc: func() error { return nil },
185+
DiffFunc: func() (string, error) { return "diff output", nil },
160186
}
161187
uiClient := &mockUIClient{
162188
TextAreaFunc: func(value string) (string, bool, error) { return "", false, nil },
@@ -175,8 +201,9 @@ func TestAppRun(t *testing.T) {
175201
callFunc: func(ctx context.Context, systemPrompt, userPrompt string) (string, error) { return "llm message", nil },
176202
}
177203
gitClient := &mockGitClient{
178-
DiffFunc: func() (string, error) { return "diff output", nil },
179-
CommitFunc: func(message string) error { return assert.AnError },
204+
IsInWorkTreeFunc: func() error { return nil },
205+
DiffFunc: func() (string, error) { return "diff output", nil },
206+
CommitFunc: func(message string) error { return assert.AnError },
180207
}
181208
uiClient := &mockUIClient{
182209
TextAreaFunc: func(value string) (string, bool, error) { return "edited message", true, nil },
@@ -196,8 +223,9 @@ func TestAppRun(t *testing.T) {
196223
callFunc: func(ctx context.Context, systemPrompt, userPrompt string) (string, error) { return "llm message", nil },
197224
}
198225
gitClient := &mockGitClient{
199-
DiffFunc: func() (string, error) { return "diff output", nil },
200-
CommitFunc: func(message string) error { return nil },
226+
IsInWorkTreeFunc: func() error { return nil },
227+
DiffFunc: func() (string, error) { return "diff output", nil },
228+
CommitFunc: func(message string) error { return nil },
201229
}
202230
uiClient := &mockUIClient{
203231
TextAreaFunc: func(value string) (string, bool, error) { return "edited message", true, nil },

internal/git/client.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package git
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"os/exec"
8+
"strings"
79
)
810

911
type Client struct{}
@@ -12,6 +14,26 @@ func NewClient() *Client {
1214
return &Client{}
1315
}
1416

17+
func (c *Client) IsInWorkTree() error {
18+
result, err := exec.Command(
19+
"git",
20+
"rev-parse",
21+
"--is-inside-work-tree",
22+
).CombinedOutput()
23+
output := strings.Trim(string(result), "\n")
24+
25+
if err != nil {
26+
fmt.Println(output)
27+
return fmt.Errorf("git rev-parse failed: %w", err)
28+
}
29+
30+
if output != "true" {
31+
return errors.New(output)
32+
}
33+
34+
return nil
35+
}
36+
1537
func (c *Client) Diff() (string, error) {
1638
result, err := exec.Command(
1739
"git",

0 commit comments

Comments
 (0)