Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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)
}
56 changes: 55 additions & 1 deletion cmd/cli/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,34 @@ import (

"github.com/99designs/keyring"

"github.com/dfanso/commit-msg/internal/cache"
"github.com/dfanso/commit-msg/pkg/types"
StoreUtils "github.com/dfanso/commit-msg/utils"
)

type StoreMethods struct {
ring keyring.Keyring
ring keyring.Keyring
cache *cache.CacheManager
}

// NewStoreMethods creates a new StoreMethods instance with cache support.
func NewStoreMethods() (*StoreMethods, error) {
ring, err := keyring.Open(keyring.Config{
ServiceName: "commit-msg",
})
if err != nil {
return nil, fmt.Errorf("failed to open keyring: %w", err)
}

cacheManager, err := cache.NewCacheManager()
if err != nil {
return nil, fmt.Errorf("failed to initialize cache: %w", err)
}

return &StoreMethods{
ring: ring,
cache: cacheManager,
}, nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Initializes Keyring instance
Expand Down Expand Up @@ -360,3 +382,35 @@ func (s *StoreMethods) UpdateAPIKey(Model types.LLMProvider, APIKey string) erro
return os.WriteFile(configPath, data, 0600)

}

// Cache management methods

// GetCacheManager returns the cache manager instance.
func (s *StoreMethods) GetCacheManager() *cache.CacheManager {
return s.cache
}

// GetCachedMessage retrieves a cached commit message if it exists.
func (s *StoreMethods) GetCachedMessage(provider types.LLMProvider, diff string, opts *types.GenerationOptions) (*types.CacheEntry, bool) {
return s.cache.Get(provider, diff, opts)
}

// SetCachedMessage stores a commit message in the cache.
func (s *StoreMethods) SetCachedMessage(provider types.LLMProvider, diff string, opts *types.GenerationOptions, message string, cost float64, tokens *types.UsageInfo) error {
return s.cache.Set(provider, diff, opts, message, cost, tokens)
}

// ClearCache removes all entries from the cache.
func (s *StoreMethods) ClearCache() error {
return s.cache.Clear()
}

// GetCacheStats returns cache statistics.
func (s *StoreMethods) GetCacheStats() *types.CacheStats {
return s.cache.GetStats()
}

// CleanupCache removes old entries from the cache.
func (s *StoreMethods) CleanupCache() error {
return s.cache.Cleanup()
}
Loading
Loading