An AI-powered tool that generates human-readable summaries of git changes using tool calling with a self-hosted LLM
at main 183 lines 4.1 kB view raw
1package cache 2 3import ( 4 "container/list" 5 "crypto/sha256" 6 "encoding/hex" 7 "fmt" 8 "log/slog" 9 "os" 10 "path/filepath" 11 "sync" 12 13 "git-summarizer/pkg/git" 14 15 "github.com/go-git/go-git/v5/plumbing/transport" 16) 17 18// RepoCache manages cached git repositories with LRU eviction 19type RepoCache struct { 20 baseDir string 21 maxRepos int 22 mu sync.Mutex 23 repoLocks map[string]*sync.Mutex 24 lru *list.List 25 repos map[string]*cacheEntry 26} 27 28type cacheEntry struct { 29 url string 30 path string 31 element *list.Element 32} 33 34// New creates a new RepoCache 35func New(baseDir string, maxRepos int) *RepoCache { 36 return &RepoCache{ 37 baseDir: baseDir, 38 maxRepos: maxRepos, 39 repoLocks: make(map[string]*sync.Mutex), 40 lru: list.New(), 41 repos: make(map[string]*cacheEntry), 42 } 43} 44 45// GetOrClone returns a cached repo or clones it if not present 46// If the repo is cached, it fetches updates before returning 47func (c *RepoCache) GetOrClone(url string, auth transport.AuthMethod) (*git.Repo, error) { 48 // Get or create per-repo lock 49 repoLock := c.getRepoLock(url) 50 repoLock.Lock() 51 defer repoLock.Unlock() 52 53 // Check if repo exists in cache 54 c.mu.Lock() 55 entry, exists := c.repos[url] 56 if exists { 57 // Move to front of LRU 58 c.lru.MoveToFront(entry.element) 59 c.mu.Unlock() 60 61 // Fetch updates 62 slog.Info("fetching cached repo", "url", url) 63 repo, err := git.Open(entry.path) 64 if err != nil { 65 // Cache entry invalid, remove and re-clone 66 slog.Warn("cached repo invalid, re-cloning", "url", url, "error", err) 67 c.mu.Lock() 68 c.removeEntry(url) 69 c.mu.Unlock() 70 return c.cloneNew(url, auth) 71 } 72 73 if err := repo.Fetch(auth); err != nil { 74 slog.Warn("fetch failed", "url", url, "error", err) 75 // Continue with potentially stale data rather than failing 76 } 77 78 return repo, nil 79 } 80 c.mu.Unlock() 81 82 // Clone new repo 83 return c.cloneNew(url, auth) 84} 85 86// cloneNew clones a repo and adds it to the cache 87func (c *RepoCache) cloneNew(url string, auth transport.AuthMethod) (*git.Repo, error) { 88 c.mu.Lock() 89 90 // Evict if at capacity 91 for c.lru.Len() >= c.maxRepos { 92 c.evictLRU() 93 } 94 95 // Prepare cache path 96 path := c.urlToPath(url) 97 c.mu.Unlock() 98 99 // Ensure cache directory exists 100 if err := os.MkdirAll(c.baseDir, 0755); err != nil { 101 return nil, fmt.Errorf("failed to create cache dir: %w", err) 102 } 103 104 // Clone 105 slog.Info("cloning repo to cache", "url", url, "path", path) 106 repo, err := git.Clone(url, path, auth) 107 if err != nil { 108 return nil, err 109 } 110 111 // Add to cache 112 c.mu.Lock() 113 entry := &cacheEntry{ 114 url: url, 115 path: path, 116 } 117 entry.element = c.lru.PushFront(url) 118 c.repos[url] = entry 119 c.mu.Unlock() 120 121 slog.Info("repo cached", "url", url, "cache_size", c.lru.Len()) 122 return repo, nil 123} 124 125// getRepoLock returns the lock for a specific repo URL 126func (c *RepoCache) getRepoLock(url string) *sync.Mutex { 127 c.mu.Lock() 128 defer c.mu.Unlock() 129 130 lock, exists := c.repoLocks[url] 131 if !exists { 132 lock = &sync.Mutex{} 133 c.repoLocks[url] = lock 134 } 135 return lock 136} 137 138// evictLRU removes the least recently used repo from the cache 139// Must be called with c.mu held 140func (c *RepoCache) evictLRU() { 141 elem := c.lru.Back() 142 if elem == nil { 143 return 144 } 145 146 url := elem.Value.(string) 147 c.removeEntry(url) 148 slog.Info("evicted repo from cache", "url", url) 149} 150 151// removeEntry removes a repo from the cache 152// Must be called with c.mu held 153func (c *RepoCache) removeEntry(url string) { 154 entry, exists := c.repos[url] 155 if !exists { 156 return 157 } 158 159 // Remove from LRU list 160 c.lru.Remove(entry.element) 161 162 // Remove from map 163 delete(c.repos, url) 164 165 // Remove from disk 166 if err := os.RemoveAll(entry.path); err != nil { 167 slog.Warn("failed to remove cached repo", "path", entry.path, "error", err) 168 } 169} 170 171// urlToPath converts a repo URL to a filesystem-safe path 172func (c *RepoCache) urlToPath(url string) string { 173 hash := sha256.Sum256([]byte(url)) 174 hashStr := hex.EncodeToString(hash[:8]) // Use first 8 bytes (16 hex chars) 175 return filepath.Join(c.baseDir, hashStr) 176} 177 178// Size returns the number of cached repos 179func (c *RepoCache) Size() int { 180 c.mu.Lock() 181 defer c.mu.Unlock() 182 return c.lru.Len() 183}