Skip to main content
Glama
git-cache-manager.ts7.41 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; import { ParsedGitUrl } from './types.js'; export interface CacheEntry { url: string; branch: string; lastUpdated: Date; lastCommit?: string; localPath: string; } export interface CacheOptions { maxAge?: number; // in milliseconds maxSize?: number; // in bytes cacheDir?: string; } export class GitCacheManager { private cacheDir: string; private options: Required<CacheOptions>; private cacheIndex: Map<string, CacheEntry>; constructor(options: CacheOptions = {}) { this.options = { maxAge: options.maxAge || 24 * 60 * 60 * 1000, // 24 hours maxSize: options.maxSize || 5 * 1024 * 1024 * 1024, // 5GB cacheDir: options.cacheDir || path.join(os.homedir(), '.coderag', 'git-cache') }; this.cacheDir = this.options.cacheDir; this.cacheIndex = new Map(); } async initialize(): Promise<void> { // Ensure cache directory exists await fs.mkdir(this.cacheDir, { recursive: true }); // Load existing cache index await this.loadCacheIndex(); // Clean up expired entries await this.cleanupExpiredEntries(); } async getCachedRepository(parsedUrl: ParsedGitUrl, branch: string = 'main'): Promise<string | null> { const cacheKey = this.generateCacheKey(parsedUrl, branch); const entry = this.cacheIndex.get(cacheKey); if (!entry) { return null; } // Check if cache entry is still valid const isExpired = Date.now() - entry.lastUpdated.getTime() > this.options.maxAge; if (isExpired) { await this.removeCacheEntry(cacheKey); return null; } // Check if cached directory still exists try { await fs.access(entry.localPath); return entry.localPath; } catch { // Directory doesn't exist, remove from cache await this.removeCacheEntry(cacheKey); return null; } } async addToCache(parsedUrl: ParsedGitUrl, branch: string, sourcePath: string): Promise<string> { const cacheKey = this.generateCacheKey(parsedUrl, branch); const targetPath = path.join(this.cacheDir, cacheKey); // Copy repository to cache await this.copyDirectory(sourcePath, targetPath); // Get last commit hash for cache validation const lastCommit = await this.getLastCommitHash(targetPath); // Create cache entry const entry: CacheEntry = { url: parsedUrl.originalUrl, branch, lastUpdated: new Date(), lastCommit, localPath: targetPath }; this.cacheIndex.set(cacheKey, entry); await this.saveCacheIndex(); return targetPath; } async updateCacheEntry(parsedUrl: ParsedGitUrl, branch: string, sourcePath: string): Promise<string> { // Remove existing entry and add new one const cacheKey = this.generateCacheKey(parsedUrl, branch); await this.removeCacheEntry(cacheKey); return this.addToCache(parsedUrl, branch, sourcePath); } async removeCacheEntry(cacheKey: string): Promise<void> { const entry = this.cacheIndex.get(cacheKey); if (entry) { try { await fs.rm(entry.localPath, { recursive: true, force: true }); } catch (error) { console.warn(`Failed to remove cached directory: ${entry.localPath}`, error); } this.cacheIndex.delete(cacheKey); await this.saveCacheIndex(); } } async clearCache(): Promise<void> { try { await fs.rm(this.cacheDir, { recursive: true, force: true }); await fs.mkdir(this.cacheDir, { recursive: true }); this.cacheIndex.clear(); await this.saveCacheIndex(); } catch (error) { console.warn('Failed to clear git cache:', error); } } async getCacheStats(): Promise<{ entries: number; totalSize: number; oldestEntry?: Date; newestEntry?: Date; }> { let totalSize = 0; let oldestEntry: Date | undefined; let newestEntry: Date | undefined; for (const entry of this.cacheIndex.values()) { try { const stats = await fs.stat(entry.localPath); totalSize += stats.size; if (!oldestEntry || entry.lastUpdated < oldestEntry) { oldestEntry = entry.lastUpdated; } if (!newestEntry || entry.lastUpdated > newestEntry) { newestEntry = entry.lastUpdated; } } catch { // Directory might not exist, will be cleaned up later } } return { entries: this.cacheIndex.size, totalSize, oldestEntry, newestEntry }; } private generateCacheKey(parsedUrl: ParsedGitUrl, branch: string): string { const key = `${parsedUrl.host}/${parsedUrl.owner}/${parsedUrl.repo}/${branch}`; return crypto.createHash('sha256').update(key).digest('hex').substring(0, 16); } private async loadCacheIndex(): Promise<void> { const indexPath = path.join(this.cacheDir, 'index.json'); try { const data = await fs.readFile(indexPath, 'utf-8'); const entries = JSON.parse(data); this.cacheIndex.clear(); for (const [key, value] of Object.entries(entries)) { this.cacheIndex.set(key, { ...value as any, lastUpdated: new Date((value as any).lastUpdated) }); } } catch { // Index file doesn't exist or is invalid, start fresh this.cacheIndex.clear(); } } private async saveCacheIndex(): Promise<void> { const indexPath = path.join(this.cacheDir, 'index.json'); const entries = Object.fromEntries(this.cacheIndex); try { await fs.writeFile(indexPath, JSON.stringify(entries, null, 2)); } catch (error) { console.warn('Failed to save cache index:', error); } } private async cleanupExpiredEntries(): Promise<void> { const now = Date.now(); const expiredKeys: string[] = []; for (const [key, entry] of this.cacheIndex.entries()) { if (now - entry.lastUpdated.getTime() > this.options.maxAge) { expiredKeys.push(key); } } for (const key of expiredKeys) { await this.removeCacheEntry(key); } } private async copyDirectory(source: string, target: string): Promise<void> { await fs.mkdir(target, { recursive: true }); const entries = await fs.readdir(source, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(source, entry.name); const targetPath = path.join(target, entry.name); if (entry.isDirectory()) { await this.copyDirectory(sourcePath, targetPath); } else { await fs.copyFile(sourcePath, targetPath); } } } private async getLastCommitHash(repoPath: string): Promise<string | undefined> { try { const gitDir = path.join(repoPath, '.git'); const headPath = path.join(gitDir, 'HEAD'); const headContent = await fs.readFile(headPath, 'utf-8'); const match = headContent.trim().match(/^ref: (.+)$/); if (match) { const refPath = path.join(gitDir, match[1]); const commit = await fs.readFile(refPath, 'utf-8'); return commit.trim(); } // HEAD contains commit hash directly return headContent.trim(); } catch { return undefined; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JonnoC/CodeRAG'

If you have feedback or need assistance with the MCP directory API, please join our Discord server