Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

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

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

## 💾 Intelligent Caching

`commit-msg` includes a smart caching system that reduces API costs and improves performance:

- **Automatic Cache Management** - Similar code changes generate the same commit message without API calls
- **Cost Savings** - Avoid redundant API requests for identical or similar changes
- **Performance Boost** - Instant retrieval of cached messages for repeated patterns
- **Cache Statistics** - Track hit rates, total savings, and cache performance
- **Secure Storage** - Cache files are stored with restricted permissions (600) for security
- **Automatic Cleanup** - Old cache entries are automatically removed based on age and usage

### Cache Management Commands

```bash
# View cache statistics
commit cache stats

# Clear all cached messages
commit cache clear

# Remove old cached messages
commit cache cleanup
```

The cache intelligently identifies similar changes by analyzing:
- File modifications and additions
- Code structure and patterns
- Commit context and style preferences
- LLM provider and generation options

---

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

---

Expand Down Expand Up @@ -256,6 +289,19 @@ Select: Change API Key
Select: Delete
```

### Cache Management

```bash
# View cache statistics and performance
commit cache stats

# Clear all cached messages
commit cache clear

# Remove old cached messages (based on age and usage)
commit cache cleanup
```

---

## Getting API Keys
Expand Down
167 changes: 167 additions & 0 deletions cmd/cli/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package cmd

import (
"fmt"
"time"

"github.com/dfanso/commit-msg/cmd/cli/store"
"github.com/pterm/pterm"
)

// ShowCacheStats displays cache statistics.
func ShowCacheStats(Store *store.StoreMethods) error {
stats := Store.GetCacheStats()

pterm.DefaultHeader.WithFullWidth().
WithBackgroundStyle(pterm.NewStyle(pterm.BgBlue)).
WithTextStyle(pterm.NewStyle(pterm.FgWhite, pterm.Bold)).
Println("Commit Message Cache Statistics")

pterm.Println()

// Create statistics table
statsData := [][]string{
{"Total Entries", fmt.Sprintf("%d", stats.TotalEntries)},
{"Cache Hits", fmt.Sprintf("%d", stats.TotalHits)},
{"Cache Misses", fmt.Sprintf("%d", stats.TotalMisses)},
{"Hit Rate", fmt.Sprintf("%.2f%%", stats.HitRate*100)},
{"Total Cost Saved", fmt.Sprintf("$%.4f", stats.TotalCostSaved)},
{"Cache Size", formatBytes(stats.CacheSizeBytes)},
}

if stats.OldestEntry != "" {
statsData = append(statsData, []string{"Oldest Entry", formatTime(stats.OldestEntry)})
}

if stats.NewestEntry != "" {
statsData = append(statsData, []string{"Newest Entry", formatTime(stats.NewestEntry)})
}

pterm.DefaultTable.WithHasHeader(false).WithData(statsData).Render()

pterm.Println()

// Show cache status
if stats.TotalEntries == 0 {
pterm.Info.Println("Cache is empty. Generate some commit messages to start building the cache.")
} else {
pterm.Success.Printf("Cache is active with %d entries\n", stats.TotalEntries)

if stats.HitRate > 0 {
pterm.Info.Printf("Cache hit rate: %.2f%% (%.0f%% of requests served from cache)\n",
stats.HitRate*100, stats.HitRate*100)
}

if stats.TotalCostSaved > 0 {
pterm.Success.Printf("Total cost saved: $%.4f\n", stats.TotalCostSaved)
}
}

return nil
}

// ClearCache removes all cached messages.
func ClearCache(Store *store.StoreMethods) error {
pterm.DefaultHeader.WithFullWidth().
WithBackgroundStyle(pterm.NewStyle(pterm.BgRed)).
WithTextStyle(pterm.NewStyle(pterm.FgWhite, pterm.Bold)).
Println("Clear Cache")

pterm.Println()

// Get current stats before clearing
stats := Store.GetCacheStats()

if stats.TotalEntries == 0 {
pterm.Info.Println("Cache is already empty.")
return nil
}

// Confirm before clearing
confirm, err := pterm.DefaultInteractiveConfirm.
WithDefaultValue(false).
Show(fmt.Sprintf("Are you sure you want to clear %d cached entries? This action cannot be undone.", stats.TotalEntries))

if err != nil {
return fmt.Errorf("failed to get confirmation: %w", err)
}

if !confirm {
pterm.Info.Println("Cache clear cancelled.")
return nil
}

// Clear the cache
if err := Store.ClearCache(); err != nil {
return fmt.Errorf("failed to clear cache: %w", err)
}

pterm.Success.Println("Cache cleared successfully!")
pterm.Info.Printf("Removed %d entries and saved $%.4f in future API costs\n",
stats.TotalEntries, stats.TotalCostSaved)

return nil
}

// CleanupCache removes old cached messages.
func CleanupCache(Store *store.StoreMethods) error {
pterm.DefaultHeader.WithFullWidth().
WithBackgroundStyle(pterm.NewStyle(pterm.BgYellow)).
WithTextStyle(pterm.NewStyle(pterm.FgBlack, pterm.Bold)).
Println("Cleanup Cache")

pterm.Println()

// Get stats before cleanup
statsBefore := Store.GetCacheStats()

if statsBefore.TotalEntries == 0 {
pterm.Info.Println("Cache is empty. Nothing to cleanup.")
return nil
}

pterm.Info.Println("Removing old and unused cached entries...")

// Perform cleanup
if err := Store.CleanupCache(); err != nil {
return fmt.Errorf("failed to cleanup cache: %w", err)
}

// Get stats after cleanup
statsAfter := Store.GetCacheStats()
removed := statsBefore.TotalEntries - statsAfter.TotalEntries

if removed > 0 {
pterm.Success.Printf("Cleanup completed! Removed %d old entries.\n", removed)
pterm.Info.Printf("Cache now contains %d entries\n", statsAfter.TotalEntries)
} else {
pterm.Info.Println("No old entries found to remove.")
}

return nil
}

// Helper functions

// formatBytes formats bytes into human-readable format.
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}

// formatTime formats a timestamp string for display.
func formatTime(timestamp string) string {
t, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
return timestamp
}
return t.Format("2006-01-02 15:04:05")
}
35 changes: 33 additions & 2 deletions cmd/cli/createMsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func CreateCommitMsg(Store *store.StoreMethods, dryRun bool, autoCommit bool) {
}

attempt := 1
commitMsg, err := generateMessage(ctx, providerInstance, changes, withAttempt(nil, attempt))
commitMsg, err := generateMessageWithCache(ctx, providerInstance, Store, commitLLM, changes, withAttempt(nil, attempt))
if err != nil {
spinnerGenerating.Fail("Failed to generate commit message")
displayProviderError(commitLLM, err)
Expand Down Expand Up @@ -182,7 +182,7 @@ interactionLoop:
pterm.Error.Printf("Failed to start spinner: %v\n", err)
continue
}
updatedMessage, genErr := generateMessage(ctx, providerInstance, changes, generationOpts)
updatedMessage, genErr := generateMessageWithCache(ctx, providerInstance, Store, commitLLM, changes, generationOpts)
if genErr != nil {
spinner.Fail("Regeneration failed")
displayProviderError(commitLLM, genErr)
Expand Down Expand Up @@ -297,6 +297,37 @@ func generateMessage(ctx context.Context, provider llm.Provider, changes string,
return provider.Generate(ctx, changes, opts)
}

// generateMessageWithCache generates a commit message with caching support.
func generateMessageWithCache(ctx context.Context, provider llm.Provider, store *store.StoreMethods, providerType types.LLMProvider, changes string, opts *types.GenerationOptions) (string, error) {
// Check cache first (only for first attempt to avoid caching regenerations)
if opts == nil || opts.Attempt <= 1 {
if cachedEntry, found := store.GetCachedMessage(providerType, changes, opts); found {
pterm.Info.Printf("Using cached commit message (saved $%.4f)\n", cachedEntry.Cost)
return cachedEntry.Message, nil
}
}

// Generate new message
message, err := provider.Generate(ctx, changes, opts)
if err != nil {
return "", err
}

// Cache the result (only for first attempt)
if opts == nil || opts.Attempt <= 1 {
// Estimate cost for caching
cost := estimateCost(providerType, estimateTokens(types.BuildCommitPrompt(changes, opts)), 100)

// Store in cache
if cacheErr := store.SetCachedMessage(providerType, changes, opts, message, cost, nil); cacheErr != nil {
// Log cache error but don't fail the generation
fmt.Printf("Warning: Failed to cache message: %v\n", cacheErr)
}
}

return message, nil
}

func promptActionSelection() (string, error) {
return pterm.DefaultInteractiveSelect.
WithOptions(actionOptions).
Expand Down
38 changes: 36 additions & 2 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"github.com/spf13/cobra"
)

//store instance
// store instance
var Store *store.StoreMethods

//Initailize store
// Initailize store
func StoreInit(sm *store.StoreMethods) {
Store = sm
}
Expand Down Expand Up @@ -65,6 +65,36 @@ var llmUpdateCmd = &cobra.Command{
},
}

var cacheCmd = &cobra.Command{
Use: "cache",
Short: "Manage commit message cache",
Long: `Manage the cache of generated commit messages to reduce API costs and improve performance.`,
}

var cacheStatsCmd = &cobra.Command{
Use: "stats",
Short: "Show cache statistics",
RunE: func(cmd *cobra.Command, args []string) error {
return ShowCacheStats(Store)
},
}

var cacheClearCmd = &cobra.Command{
Use: "clear",
Short: "Clear all cached messages",
RunE: func(cmd *cobra.Command, args []string) error {
return ClearCache(Store)
},
}

var cacheCleanupCmd = &cobra.Command{
Use: "cleanup",
Short: "Remove old cached messages",
RunE: func(cmd *cobra.Command, args []string) error {
return CleanupCache(Store)
},
}

var creatCommitMsg = &cobra.Command{
Use: ".",
Short: "Create Commit Message",
Expand Down Expand Up @@ -101,6 +131,10 @@ func init() {

rootCmd.AddCommand(creatCommitMsg)
rootCmd.AddCommand(llmCmd)
rootCmd.AddCommand(cacheCmd)
llmCmd.AddCommand(llmSetupCmd)
llmCmd.AddCommand(llmUpdateCmd)
cacheCmd.AddCommand(cacheStatsCmd)
cacheCmd.AddCommand(cacheClearCmd)
cacheCmd.AddCommand(cacheCleanupCmd)
}
Loading
Loading