Skip to content

Commit 4f9a91b

Browse files
committed
refactor 🎨 (cmd): add detailed commit message generation
• Implement new `generateBody` flag to control detailed commit body generation • Modify `draftCmd` to handle both regular and detailed commit messages • Add `CommitResult` struct to store detailed commit message components • Implement `GenerateDetailedCommit` method in AI providers for enhanced message generation • Enhance diff summarization logic to provide more context for AI models • Update usage instructions to inform users about new options Signed-off-by: Zine Moualhi 🇵🇸 <zmoualhi@outlook.com>
1 parent e920880 commit 4f9a91b

File tree

5 files changed

+589
-24
lines changed

5 files changed

+589
-24
lines changed

cmd/draft.go

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var (
2020
overrideType string
2121
overrideScope string
2222
extraContext string
23+
generateBody bool
2324
)
2425

2526
func printErrorAndExit(format string, a ...interface{}) {
@@ -120,19 +121,34 @@ var draftCmd = &cobra.Command{
120121

121122
fmt.Println(mutedStyle.Render(fmt.Sprintf("🤖 Generating commit message using %s...", provider.GetModel())))
122123

123-
// Pass the extra context to the AI provider
124-
commitMessage, err := provider.GenerateCommitMessage(diff, string(commitTypesJSON), extraContext)
124+
// Handle both regular and detailed commit generation
125+
var finalCommitMessage, commitBody string
126+
if generateBody {
127+
// Generate detailed commit with body
128+
result, err := provider.GenerateDetailedCommit(diff, string(commitTypesJSON), extraContext)
129+
if err != nil {
130+
printErrorAndExit("❌ Error generating detailed commit message: %v", err)
131+
}
125132

126-
if err != nil {
127-
printErrorAndExit("❌ Error generating commit message: %v", err)
128-
}
133+
if result.Message == "" {
134+
printErrorAndExit("❌ No commit message generated. The AI provider returned an empty response.")
135+
}
129136

130-
// Check if we got a valid commit message
131-
if commitMessage == "" {
132-
printErrorAndExit("❌ No commit message generated. The AI provider returned an empty response.")
133-
}
137+
finalCommitMessage = processCommitMessage(result.Message, cfg.NoEmoji, cfg.Types)
138+
commitBody = result.Body
139+
} else {
140+
// Generate simple commit message
141+
commitMessage, err := provider.GenerateCommitMessage(diff, string(commitTypesJSON), extraContext)
142+
if err != nil {
143+
printErrorAndExit("❌ Error generating commit message: %v", err)
144+
}
134145

135-
finalCommitMessage := processCommitMessage(commitMessage, cfg.NoEmoji, cfg.Types)
146+
if commitMessage == "" {
147+
printErrorAndExit("❌ No commit message generated. The AI provider returned an empty response.")
148+
}
149+
150+
finalCommitMessage = processCommitMessage(commitMessage, cfg.NoEmoji, cfg.Types)
151+
}
136152

137153
// Validate the final commit message
138154
if finalCommitMessage == "" {
@@ -142,25 +158,36 @@ var draftCmd = &cobra.Command{
142158
fmt.Println(successMsgStyle.Render("✅ Commit message generated successfully!"))
143159

144160
if commitDirectly {
145-
fmt.Println(commitMsgStyle.Render(finalCommitMessage))
161+
// Display the commit message with body if available
162+
displayMessage := finalCommitMessage
163+
if commitBody != "" {
164+
displayMessage += "\n\n" + commitBody
165+
}
166+
fmt.Println(commitMsgStyle.Render(displayMessage))
146167

147168
fmt.Println(mutedStyle.Render("📤 Committing changes..."))
148169

149-
err := executeGitCommit(finalCommitMessage, "", cfg.SignOff)
150-
170+
err := executeGitCommit(finalCommitMessage, commitBody, cfg.SignOff)
151171
if err != nil {
152172
printErrorAndExit("❌ Error committing changes: %v", err)
153173
}
154174
fmt.Println(successMsgStyle.Render("🎉 Successfully committed changes!"))
155175
} else {
156-
157176
fmt.Println(infoMsgStyle.Render("Here's your generated commit message:"))
158177
fmt.Println(commitMsgStyle.Render(finalCommitMessage))
178+
if commitBody != "" {
179+
fmt.Println(commitMsgStyle.Render(commitBody))
180+
}
181+
182+
bodyHint := ""
183+
if !generateBody {
184+
bodyHint = "\n • Use --body flag to generate detailed commit body"
185+
}
159186
fmt.Println(infoMsgStyle.Render(
160187
"💡 Ready to commit!\n" +
161188
" • Run with --commit flag to auto-commit\n" +
162189
" • Use --type and --scope to override defaults\n" +
163-
" • Use --context to provide additional context to the AI", // Update usage info
190+
" • Use --context to provide additional context to the AI" + bodyHint,
164191
))
165192
}
166193
},
@@ -171,6 +198,6 @@ func init() {
171198
draftCmd.Flags().BoolVarP(&commitDirectly, "commit", "c", false, "Commit the generated message directly")
172199
draftCmd.Flags().StringVarP(&overrideType, "type", "t", "", "Override the commit type (e.g., feat, fix, docs)")
173200
draftCmd.Flags().StringVarP(&overrideScope, "scope", "s", "", "Override the commit scope (e.g., api, ui, core)")
174-
// New: Add the --context flag
175201
draftCmd.Flags().StringVarP(&extraContext, "context", "x", "", "Provide additional context for AI commit message generation")
202+
draftCmd.Flags().BoolVarP(&generateBody, "body", "b", false, "Generate detailed commit body with bullet points explaining the changes")
176203
}

pkg/ai/ai.go

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

3+
type CommitResult struct {
4+
Message string
5+
Body string
6+
}
7+
38
type AIProvider interface {
49
GenerateCommitMessage(diff string, commitTypes string, extraContext string) (string, error)
10+
GenerateDetailedCommit(diff string, commitTypes string, extraContext string) (*CommitResult, error)
511
GetModel() string
612
}

pkg/ai/openrouter.go

Lines changed: 150 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,25 @@ func (p *OpenRouterProvider) GenerateCommitMessage(diff string, commitTypesJSON
7676
return "", fmt.Errorf("empty diff provided")
7777
}
7878

79-
// Enhance small diffs
80-
diff = enhanceSmallDiff(diff)
79+
// Use smart diff summarization for better AI comprehension
80+
diffSummary, optimizedDiff := summarizeDiff(diff)
8181

82-
// Truncate large diffs
83-
originalDiffSize := len(diff)
84-
diff = truncateDiff(diff)
85-
if originalDiffSize != len(diff) {
86-
// Add warning to extra context if diff was truncated
82+
// Add summary info to context if diff was significantly reduced
83+
if diffSummary.SummarySize < diffSummary.OriginalSize {
8784
if extraContext != "" {
8885
extraContext += " "
8986
}
90-
extraContext += fmt.Sprintf("(Note: Diff was truncated from %d to %d characters due to size limits)", originalDiffSize, len(diff))
87+
extraContext += fmt.Sprintf("(Diff summarized: %d files changed. %s)",
88+
len(diffSummary.FilesChanged),
89+
diffSummary.Summary)
9190
}
9291

92+
// Use the optimized diff
93+
diff = optimizedDiff
94+
95+
// Still apply small diff enhancement if needed
96+
diff = enhanceSmallDiff(diff)
97+
9398
systemPrompt := `You are a commit message generator that follows these rules:
9499
1. Write in present tense
95100
2. Be concise and direct
@@ -182,6 +187,143 @@ Code diff:`
182187
return "", fmt.Errorf("no valid commit message found in response. Raw response: %s", rawResult)
183188
}
184189

190+
func (p *OpenRouterProvider) GenerateDetailedCommit(diff string, commitTypesJSON string, extraContext string) (*CommitResult, error) {
191+
// Validate and enhance the diff
192+
if diff == "" {
193+
return nil, fmt.Errorf("empty diff provided")
194+
}
195+
196+
// Use smart diff summarization for better AI comprehension
197+
diffSummary, optimizedDiff := summarizeDiff(diff)
198+
199+
// Add summary info to context if diff was significantly reduced
200+
if diffSummary.SummarySize < diffSummary.OriginalSize {
201+
if extraContext != "" {
202+
extraContext += " "
203+
}
204+
extraContext += fmt.Sprintf("(Diff summarized: %d files changed. %s)",
205+
len(diffSummary.FilesChanged),
206+
diffSummary.Summary)
207+
}
208+
209+
// Use the optimized diff
210+
diff = optimizedDiff
211+
diff = enhanceSmallDiff(diff)
212+
213+
systemPrompt := `You are a commit message generator that generates both a concise title and a detailed body.
214+
215+
RULES FOR TITLE:
216+
1. Write in present tense
217+
2. Be concise and direct
218+
3. Follow the format: <type>(<optional scope>): <commit message>
219+
4. Keep the title under 72 characters
220+
5. Focus on what changed, not how it changed
221+
222+
RULES FOR BODY:
223+
1. Write in present tense
224+
2. Provide 2-4 bullet points that explain the changes in depth
225+
3. Each bullet point should be concise but informative
226+
4. Focus on the WHY and WHAT of the changes
227+
5. Reference specific files or functionality when relevant
228+
229+
VALID TYPES: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
230+
231+
OUTPUT FORMAT:
232+
Title: <type>(<optional scope>): <commit message>
233+
234+
Body:
235+
• First detailed point about the changes
236+
• Second detailed point about the changes
237+
• Third detailed point about the changes (if applicable)
238+
239+
EXAMPLE OUTPUT:
240+
Title: feat(auth): add user authentication system
241+
242+
Body:
243+
• Add JWT token generation and validation middleware
244+
• Implement login/logout endpoints with secure password hashing
245+
• Create user model with role-based access control
246+
• Add authentication middleware to protect API routes`
247+
248+
userPrompt := fmt.Sprintf(`Generate a commit message with detailed body for the following code diff:
249+
250+
Follow the exact format specified. Include both a concise title and detailed bullet points in the body.
251+
252+
Choose a type from the type-to-description JSON below that best describes the git diff:
253+
%s
254+
`, commitTypesJSON)
255+
256+
if extraContext != "" {
257+
userPrompt += fmt.Sprintf("\nAdditional context: %s\n", extraContext)
258+
}
259+
260+
userPrompt += `Focus on being accurate and comprehensive.
261+
Code diff:`
262+
userPrompt += fmt.Sprintf("\n```diff\n%s\n```", diff)
263+
264+
requestPayload := OpenRouterRequest{
265+
Model: p.model,
266+
Messages: []OpenRouterMessage{
267+
{Role: "system", Content: systemPrompt},
268+
{Role: "user", Content: userPrompt},
269+
},
270+
}
271+
272+
jsonPayload, err := json.Marshal(requestPayload)
273+
if err != nil {
274+
return nil, fmt.Errorf("failed to marshal OpenRouter request payload: %w", err)
275+
}
276+
277+
req, err := http.NewRequest("POST", openRouterAPIURL, bytes.NewBuffer(jsonPayload))
278+
if err != nil {
279+
return nil, fmt.Errorf("failed to create OpenRouter request: %w", err)
280+
}
281+
282+
req.Header.Set("Authorization", "Bearer "+p.apiKey)
283+
req.Header.Set("Content-Type", "application/json")
284+
req.Header.Set("HTTP-Referer", httpReferer)
285+
req.Header.Set("X-Title", xTitle)
286+
req.Header.Set("Accept", "application/json")
287+
288+
resp, err := p.client.Do(req)
289+
if err != nil {
290+
return nil, fmt.Errorf("failed to send request to OpenRouter: %w", err)
291+
}
292+
defer resp.Body.Close()
293+
294+
bodyBytes, err := io.ReadAll(resp.Body)
295+
if err != nil {
296+
return nil, fmt.Errorf("failed to read OpenRouter response body: %w", err)
297+
}
298+
299+
if resp.StatusCode != http.StatusOK {
300+
var errorResponse OpenRouterErrorResponse
301+
if err := json.Unmarshal(bodyBytes, &errorResponse); err == nil && errorResponse.Error.Message != "" {
302+
return nil, fmt.Errorf("openrouter API error (status %d): %s", resp.StatusCode, errorResponse.Error.Message)
303+
}
304+
return nil, fmt.Errorf("openrouter API returned status %d: %s", resp.StatusCode, string(bodyBytes))
305+
}
306+
307+
var successResponse OpenRouterResponse
308+
if err := json.Unmarshal(bodyBytes, &successResponse); err != nil {
309+
return nil, fmt.Errorf("failed to unmarshal OpenRouter success response: %w. Body: %s", err, string(bodyBytes))
310+
}
311+
312+
if len(successResponse.Choices) == 0 || successResponse.Choices[0].Message.Content == "" {
313+
return nil, fmt.Errorf("no content found in OpenRouter response or choices array is empty. Body: %s", string(bodyBytes))
314+
}
315+
316+
rawResult := successResponse.Choices[0].Message.Content
317+
318+
// Parse the detailed commit message to extract title and body
319+
result := parseDetailedCommitMessage(rawResult)
320+
if result.Message == "" {
321+
return nil, fmt.Errorf("no valid commit message found in response. Raw response: %s", rawResult)
322+
}
323+
324+
return result, nil
325+
}
326+
185327
func (p *OpenRouterProvider) GetModel() string {
186328
return p.model
187329
}

0 commit comments

Comments
 (0)