-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
258 lines (220 loc) · 6.67 KB
/
Copy pathmain.go
File metadata and controls
258 lines (220 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
)
type OllamaRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Stream bool `json:"stream"`
Options struct {
Temperature float64 `json:"temperature"`
TopP float64 `json:"top_p"`
} `json:"options"`
}
type OllamaResponse struct {
Model string `json:"model"`
CreatedAt string `json:"created_at"`
Response string `json:"response"`
Done bool `json:"done"`
Context []int `json:"context"`
TotalDuration int64 `json:"total_duration"`
LoadDuration int64 `json:"load_duration"`
PromptEvalCount int `json:"prompt_eval_count"`
PromptEvalDuration int64 `json:"prompt_eval_duration"`
EvalCount int `json:"eval_count"`
EvalDuration int64 `json:"eval_duration"`
}
func main() {
var rootCmd = &cobra.Command{
Use: "gencommit",
Short: "Generate commit message using Ollama",
Long: `Generate a commit message based on git diff using Ollama's deepseek-r1:8b model`,
Run: runGenCommit,
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runGenCommit(cmd *cobra.Command, args []string) {
// Check if we're in a git repository
if !isGitRepo() {
fmt.Fprintf(os.Stderr, "Error: Not in a git repository\n")
os.Exit(1)
}
// Get git diff
diff, err := getGitDiff()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting git diff: %v\n", err)
os.Exit(1)
}
if diff == "" {
fmt.Println("No changes detected. Nothing to commit.")
return
}
// Generate commit message using Ollama
commitMsg, err := generateCommitMessage(diff)
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating commit message: %v\n", err)
os.Exit(1)
}
// Limit to 20 words
commitMsg = limitWords(commitMsg, 20)
fmt.Printf("Generated commit message: %s\n", commitMsg)
fmt.Println("\nTo commit with this message, run:")
fmt.Printf("git commit -m \"%s\"\n", commitMsg)
}
func isGitRepo() bool {
cmd := exec.Command("git", "rev-parse", "--git-dir")
return cmd.Run() == nil
}
func getGitDiff() (string, error) {
// Get staged changes first
cmd := exec.Command("git", "diff", "--cached")
output, err := cmd.Output()
if err != nil {
// If no staged changes, get unstaged changes
cmd = exec.Command("git", "diff")
output, err = cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git diff: %w", err)
}
}
return string(output), nil
}
func generateCommitMessage(diff string) (string, error) {
// Prepare the prompt for Ollama
prompt := fmt.Sprintf(`Generate a concise commit message based on this git diff.
IMPORTANT: Return ONLY the commit message text. Do not include:
- Any explanations
- Thinking process
- Tags like <think> or <thinking>
- Introductory phrases like "Based on the diff" or "The changes show"
Git diff:
%s
Commit message:`, diff)
// Create Ollama request
request := OllamaRequest{
Model: "deepseek-r1:8b",
Prompt: prompt,
Stream: false,
}
request.Options.Temperature = 0.7
request.Options.TopP = 0.9
// Convert request to JSON
jsonData, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Send request to Ollama
resp, err := http.Post("http://localhost:11434/api/generate", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to connect to Ollama: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("ollama API error: %s - %s", resp.Status, string(body))
}
// Parse response
var ollamaResp OllamaResponse
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
// Clean up the response
commitMsg := strings.TrimSpace(ollamaResp.Response)
// Remove any <think> blocks that might appear
commitMsg = removeThinkBlocks(commitMsg)
// Remove any remaining thinking-related text patterns
commitMsg = removeThinkingPatterns(commitMsg)
// Clean up formatting
commitMsg = strings.ReplaceAll(commitMsg, "\n", " ")
commitMsg = strings.ReplaceAll(commitMsg, "\"", "'")
commitMsg = strings.TrimSpace(commitMsg)
return commitMsg, nil
}
func removeThinkBlocks(text string) string {
// Remove <think>...</think> blocks
start := strings.Index(text, "<think>")
if start == -1 {
// Also check for variations like <thinking>, <thought>, etc.
start = strings.Index(text, "<thinking>")
if start == -1 {
start = strings.Index(text, "<thought>")
if start == -1 {
start = strings.Index(text, "<reasoning>")
if start == -1 {
return text
}
}
}
}
// Find the corresponding closing tag
var end int
var tagLength int
if strings.HasPrefix(text[start:], "<think>") {
end = strings.Index(text, "</think>")
tagLength = 8 // length of "</think>"
} else if strings.HasPrefix(text[start:], "<thinking>") {
end = strings.Index(text, "</thinking>")
tagLength = 11 // length of "</thinking>"
} else if strings.HasPrefix(text[start:], "<thought>") {
end = strings.Index(text, "</thought>")
tagLength = 10 // length of "</thought>"
} else if strings.HasPrefix(text[start:], "<reasoning>") {
end = strings.Index(text, "</reasoning>")
tagLength = 12 // length of "</reasoning>"
}
if end == -1 {
// If no closing tag, just remove from start to end
return strings.TrimSpace(text[:start])
}
// Remove the think block and clean up
result := text[:start] + text[end+tagLength:]
return strings.TrimSpace(result)
}
func removeThinkingPatterns(text string) string {
// Remove common thinking patterns that might appear
patterns := []string{
"Let me think about this",
"Let me analyze",
"Based on the diff",
"Looking at the changes",
"The changes show",
"I can see that",
"This appears to be",
"From the diff",
}
result := text
for _, pattern := range patterns {
if strings.HasPrefix(strings.ToLower(result), strings.ToLower(pattern)) {
// Find the end of the sentence or line
periodIndex := strings.Index(result, ".")
newlineIndex := strings.Index(result, "\n")
if periodIndex != -1 && (newlineIndex == -1 || periodIndex < newlineIndex) {
result = strings.TrimSpace(result[periodIndex+1:])
} else if newlineIndex != -1 {
result = strings.TrimSpace(result[newlineIndex+1:])
} else {
result = strings.TrimSpace(result[len(pattern):])
}
break
}
}
return result
}
func limitWords(text string, maxWords int) string {
words := strings.Fields(text)
if len(words) <= maxWords {
return text
}
return strings.Join(words[:maxWords], " ")
}