Skip to content

Commit 9138c7b

Browse files
authored
Merge pull request #118 from Laaaaksh/feature/commit-message-caching
2 parents 203787b + 1dedb59 commit 9138c7b

File tree

10 files changed

+1384
-8
lines changed

10 files changed

+1384
-8
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Looking to contribute? Check out:
3535
🎛️ **Interactive Review Flow** - Accept, regenerate with new styles, or open the message in your editor before committing
3636
📊 **File Statistics Display** - Visual preview of changed files and line counts
3737
💡 **Smart Security Scrubbing** - Automatically removes API keys, passwords, and sensitive data from diffs
38+
💾 **Intelligent Caching** - Reduces API costs by caching generated messages for similar changes
3839
🚀 **Easy to Use** - Simple CLI interface with beautiful terminal UI
3940
⚡️ **Fast** - Quick generation of commit messages
4041

@@ -53,6 +54,36 @@ You can use **Google Gemini**, **Grok**, **Claude**, **ChatGPT**, or **Ollama**
5354

5455
All scrubbing happens locally before any data leaves your machine, ensuring your secrets stay secure.
5556

57+
## 💾 Intelligent Caching
58+
59+
`commit-msg` includes a smart caching system that reduces API costs and improves performance:
60+
61+
- **Automatic Cache Management** - Similar code changes generate the same commit message without API calls
62+
- **Cost Savings** - Avoid redundant API requests for identical or similar changes
63+
- **Performance Boost** - Instant retrieval of cached messages for repeated patterns
64+
- **Cache Statistics** - Track hit rates, total savings, and cache performance
65+
- **Secure Storage** - Cache files are stored with restricted permissions (600) for security
66+
- **Automatic Cleanup** - Old cache entries are automatically removed based on age and usage
67+
68+
### Cache Management Commands
69+
70+
```bash
71+
# View cache statistics
72+
commit cache stats
73+
74+
# Clear all cached messages
75+
commit cache clear
76+
77+
# Remove old cached messages
78+
commit cache cleanup
79+
```
80+
81+
The cache intelligently identifies similar changes by analyzing:
82+
- File modifications and additions
83+
- Code structure and patterns
84+
- Commit context and style preferences
85+
- LLM provider and generation options
86+
5687
---
5788

5889
## 📦 Installation
@@ -221,6 +252,8 @@ This makes it easy to tweak the tone, iterate on suggestions, or fine-tune the f
221252
- ✅ Create conventional commit messages
222253
- 📋 Auto-copy to clipboard for immediate use
223254
- 🎨 Beautiful terminal UI with file statistics and previews
255+
- 💾 Reduce API costs with intelligent caching for similar changes
256+
- ⚡️ Get instant results for repeated code patterns
224257

225258
---
226259

@@ -256,6 +289,19 @@ Select: Change API Key
256289
Select: Delete
257290
```
258291

292+
### Cache Management
293+
294+
```bash
295+
# View cache statistics and performance
296+
commit cache stats
297+
298+
# Clear all cached messages
299+
commit cache clear
300+
301+
# Remove old cached messages (based on age and usage)
302+
commit cache cleanup
303+
```
304+
259305
---
260306

261307
## Getting API Keys

cmd/cli/cache.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/dfanso/commit-msg/cmd/cli/store"
8+
"github.com/pterm/pterm"
9+
)
10+
11+
// ShowCacheStats displays cache statistics.
12+
func ShowCacheStats(Store *store.StoreMethods) error {
13+
stats := Store.GetCacheStats()
14+
15+
pterm.DefaultHeader.WithFullWidth().
16+
WithBackgroundStyle(pterm.NewStyle(pterm.BgBlue)).
17+
WithTextStyle(pterm.NewStyle(pterm.FgWhite, pterm.Bold)).
18+
Println("Commit Message Cache Statistics")
19+
20+
pterm.Println()
21+
22+
// Create statistics table
23+
statsData := [][]string{
24+
{"Total Entries", fmt.Sprintf("%d", stats.TotalEntries)},
25+
{"Cache Hits", fmt.Sprintf("%d", stats.TotalHits)},
26+
{"Cache Misses", fmt.Sprintf("%d", stats.TotalMisses)},
27+
{"Hit Rate", fmt.Sprintf("%.2f%%", stats.HitRate*100)},
28+
{"Total Cost Saved", fmt.Sprintf("$%.4f", stats.TotalCostSaved)},
29+
{"Cache Size", formatBytes(stats.CacheSizeBytes)},
30+
}
31+
32+
if stats.OldestEntry != "" {
33+
statsData = append(statsData, []string{"Oldest Entry", formatTime(stats.OldestEntry)})
34+
}
35+
36+
if stats.NewestEntry != "" {
37+
statsData = append(statsData, []string{"Newest Entry", formatTime(stats.NewestEntry)})
38+
}
39+
40+
pterm.DefaultTable.WithHasHeader(false).WithData(statsData).Render()
41+
42+
pterm.Println()
43+
44+
// Show cache status
45+
if stats.TotalEntries == 0 {
46+
pterm.Info.Println("Cache is empty. Generate some commit messages to start building the cache.")
47+
} else {
48+
pterm.Success.Printf("Cache is active with %d entries\n", stats.TotalEntries)
49+
50+
if stats.HitRate > 0 {
51+
pterm.Info.Printf("Cache hit rate: %.2f%% (%.0f%% of requests served from cache)\n",
52+
stats.HitRate*100, stats.HitRate*100)
53+
}
54+
55+
if stats.TotalCostSaved > 0 {
56+
pterm.Success.Printf("Total cost saved: $%.4f\n", stats.TotalCostSaved)
57+
}
58+
}
59+
60+
return nil
61+
}
62+
63+
// ClearCache removes all cached messages.
64+
func ClearCache(Store *store.StoreMethods) error {
65+
pterm.DefaultHeader.WithFullWidth().
66+
WithBackgroundStyle(pterm.NewStyle(pterm.BgRed)).
67+
WithTextStyle(pterm.NewStyle(pterm.FgWhite, pterm.Bold)).
68+
Println("Clear Cache")
69+
70+
pterm.Println()
71+
72+
// Get current stats before clearing
73+
stats := Store.GetCacheStats()
74+
75+
if stats.TotalEntries == 0 {
76+
pterm.Info.Println("Cache is already empty.")
77+
return nil
78+
}
79+
80+
// Confirm before clearing
81+
confirm, err := pterm.DefaultInteractiveConfirm.
82+
WithDefaultValue(false).
83+
Show(fmt.Sprintf("Are you sure you want to clear %d cached entries? This action cannot be undone.", stats.TotalEntries))
84+
85+
if err != nil {
86+
return fmt.Errorf("failed to get confirmation: %w", err)
87+
}
88+
89+
if !confirm {
90+
pterm.Info.Println("Cache clear cancelled.")
91+
return nil
92+
}
93+
94+
// Clear the cache
95+
if err := Store.ClearCache(); err != nil {
96+
return fmt.Errorf("failed to clear cache: %w", err)
97+
}
98+
99+
pterm.Success.Println("Cache cleared successfully!")
100+
pterm.Info.Printf("Removed %d entries and saved $%.4f in future API costs\n",
101+
stats.TotalEntries, stats.TotalCostSaved)
102+
103+
return nil
104+
}
105+
106+
// CleanupCache removes old cached messages.
107+
func CleanupCache(Store *store.StoreMethods) error {
108+
pterm.DefaultHeader.WithFullWidth().
109+
WithBackgroundStyle(pterm.NewStyle(pterm.BgYellow)).
110+
WithTextStyle(pterm.NewStyle(pterm.FgBlack, pterm.Bold)).
111+
Println("Cleanup Cache")
112+
113+
pterm.Println()
114+
115+
// Get stats before cleanup
116+
statsBefore := Store.GetCacheStats()
117+
118+
if statsBefore.TotalEntries == 0 {
119+
pterm.Info.Println("Cache is empty. Nothing to cleanup.")
120+
return nil
121+
}
122+
123+
pterm.Info.Println("Removing old and unused cached entries...")
124+
125+
// Perform cleanup
126+
if err := Store.CleanupCache(); err != nil {
127+
return fmt.Errorf("failed to cleanup cache: %w", err)
128+
}
129+
130+
// Get stats after cleanup
131+
statsAfter := Store.GetCacheStats()
132+
removed := statsBefore.TotalEntries - statsAfter.TotalEntries
133+
134+
if removed > 0 {
135+
pterm.Success.Printf("Cleanup completed! Removed %d old entries.\n", removed)
136+
pterm.Info.Printf("Cache now contains %d entries\n", statsAfter.TotalEntries)
137+
} else {
138+
pterm.Info.Println("No old entries found to remove.")
139+
}
140+
141+
return nil
142+
}
143+
144+
// Helper functions
145+
146+
// formatBytes formats bytes into human-readable format.
147+
func formatBytes(bytes int64) string {
148+
const unit = 1024
149+
if bytes < unit {
150+
return fmt.Sprintf("%d B", bytes)
151+
}
152+
div, exp := int64(unit), 0
153+
for n := bytes / unit; n >= unit; n /= unit {
154+
div *= unit
155+
exp++
156+
}
157+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
158+
}
159+
160+
// formatTime formats a timestamp string for display.
161+
func formatTime(timestamp string) string {
162+
t, err := time.Parse(time.RFC3339, timestamp)
163+
if err != nil {
164+
return timestamp
165+
}
166+
return t.Format("2006-01-02 15:04:05")
167+
}

cmd/cli/createMsg.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func CreateCommitMsg(Store *store.StoreMethods, dryRun bool, autoCommit bool) {
154154
}
155155

156156
attempt := 1
157-
commitMsg, err := generateMessage(ctx, providerInstance, changes, withAttempt(nil, attempt))
157+
commitMsg, err := generateMessageWithCache(ctx, providerInstance, Store, commitLLM, changes, withAttempt(nil, attempt))
158158
if err != nil {
159159
spinnerGenerating.Fail("Failed to generate commit message")
160160
displayProviderError(commitLLM, err)
@@ -217,7 +217,7 @@ interactionLoop:
217217
pterm.Error.Printf("Failed to start spinner: %v\n", err)
218218
continue
219219
}
220-
updatedMessage, genErr := generateMessage(ctx, providerInstance, changes, generationOpts)
220+
updatedMessage, genErr := generateMessageWithCache(ctx, providerInstance, Store, commitLLM, changes, generationOpts)
221221
if genErr != nil {
222222
spinner.Fail("Regeneration failed")
223223
displayProviderError(commitLLM, genErr)
@@ -332,6 +332,37 @@ func generateMessage(ctx context.Context, provider llm.Provider, changes string,
332332
return provider.Generate(ctx, changes, opts)
333333
}
334334

335+
// generateMessageWithCache generates a commit message with caching support.
336+
func generateMessageWithCache(ctx context.Context, provider llm.Provider, store *store.StoreMethods, providerType types.LLMProvider, changes string, opts *types.GenerationOptions) (string, error) {
337+
// Check cache first (only for first attempt to avoid caching regenerations)
338+
if opts == nil || opts.Attempt <= 1 {
339+
if cachedEntry, found := store.GetCachedMessage(providerType, changes, opts); found {
340+
pterm.Info.Printf("Using cached commit message (saved $%.4f)\n", cachedEntry.Cost)
341+
return cachedEntry.Message, nil
342+
}
343+
}
344+
345+
// Generate new message
346+
message, err := provider.Generate(ctx, changes, opts)
347+
if err != nil {
348+
return "", err
349+
}
350+
351+
// Cache the result (only for first attempt)
352+
if opts == nil || opts.Attempt <= 1 {
353+
// Estimate cost for caching
354+
cost := estimateCost(providerType, estimateTokens(types.BuildCommitPrompt(changes, opts)), 100)
355+
356+
// Store in cache
357+
if cacheErr := store.SetCachedMessage(providerType, changes, opts, message, cost, nil); cacheErr != nil {
358+
// Log cache error but don't fail the generation
359+
fmt.Printf("Warning: Failed to cache message: %v\n", cacheErr)
360+
}
361+
}
362+
363+
return message, nil
364+
}
365+
335366
func promptActionSelection() (string, error) {
336367
return pterm.DefaultInteractiveSelect.
337368
WithOptions(actionOptions).

cmd/cli/root.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import (
77
"github.com/spf13/cobra"
88
)
99

10-
//store instance
10+
// store instance
1111
var Store *store.StoreMethods
1212

13-
//Initailize store
13+
// Initailize store
1414
func StoreInit(sm *store.StoreMethods) {
1515
Store = sm
1616
}
@@ -65,6 +65,36 @@ var llmUpdateCmd = &cobra.Command{
6565
},
6666
}
6767

68+
var cacheCmd = &cobra.Command{
69+
Use: "cache",
70+
Short: "Manage commit message cache",
71+
Long: `Manage the cache of generated commit messages to reduce API costs and improve performance.`,
72+
}
73+
74+
var cacheStatsCmd = &cobra.Command{
75+
Use: "stats",
76+
Short: "Show cache statistics",
77+
RunE: func(cmd *cobra.Command, args []string) error {
78+
return ShowCacheStats(Store)
79+
},
80+
}
81+
82+
var cacheClearCmd = &cobra.Command{
83+
Use: "clear",
84+
Short: "Clear all cached messages",
85+
RunE: func(cmd *cobra.Command, args []string) error {
86+
return ClearCache(Store)
87+
},
88+
}
89+
90+
var cacheCleanupCmd = &cobra.Command{
91+
Use: "cleanup",
92+
Short: "Remove old cached messages",
93+
RunE: func(cmd *cobra.Command, args []string) error {
94+
return CleanupCache(Store)
95+
},
96+
}
97+
6898
var creatCommitMsg = &cobra.Command{
6999
Use: ".",
70100
Short: "Create Commit Message",
@@ -101,6 +131,10 @@ func init() {
101131

102132
rootCmd.AddCommand(creatCommitMsg)
103133
rootCmd.AddCommand(llmCmd)
134+
rootCmd.AddCommand(cacheCmd)
104135
llmCmd.AddCommand(llmSetupCmd)
105136
llmCmd.AddCommand(llmUpdateCmd)
137+
cacheCmd.AddCommand(cacheStatsCmd)
138+
cacheCmd.AddCommand(cacheClearCmd)
139+
cacheCmd.AddCommand(cacheCleanupCmd)
106140
}

0 commit comments

Comments
 (0)