UNPKG

git-tweezers

Version:

Advanced git staging tool with hunk and line-level control

187 lines (186 loc) 6.47 kB
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { GitWrapper } from '../core/git-wrapper.js'; import { generateContentFingerprint } from '../core/hunk-id.js'; export class HunkCacheService { cachePath; git; cacheData; constructor(cwd) { this.git = new GitWrapper(cwd); const gitDir = this.git.getGitDir(); this.cachePath = join(gitDir, 'tweezers-cache.json'); // Debug logging for worktree issues if (process.env.DEBUG) { console.error(`[HunkCacheService] cwd: ${cwd}`); console.error(`[HunkCacheService] gitDir: ${gitDir}`); console.error(`[HunkCacheService] cachePath: ${this.cachePath}`); } this.cacheData = this.loadCache(); } loadCache() { if (!existsSync(this.cachePath)) { return { version: 2, fingerprints: {}, usedIds: {}, history: [] }; } try { const content = readFileSync(this.cachePath, 'utf8'); const data = JSON.parse(content); // Migrate from version 1 to version 2 if (data.version === 1) { return { version: 2, fingerprints: {}, usedIds: {}, history: data.history || [] }; } // Ensure history array exists if (!data.history) { data.history = []; } // Ensure required fields exist if (!data.fingerprints) data.fingerprints = {}; if (!data.usedIds) data.usedIds = {}; return data; } catch { return { version: 2, fingerprints: {}, usedIds: {}, history: [] }; } } saveCache() { const dir = dirname(this.cachePath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } // Clean up old entries (older than 7 days) const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; Object.keys(this.cacheData.usedIds).forEach(id => { const entry = this.cacheData.usedIds[id]; if (entry.lastSeen < weekAgo) { // Remove from both maps delete this.cacheData.usedIds[id]; delete this.cacheData.fingerprints[entry.fingerprint]; } }); writeFileSync(this.cachePath, JSON.stringify(this.cacheData, null, 2)); } /** * Get or create stable ID mapping for hunks */ mapHunks(filePath, hunks) { const now = Date.now(); const existingIds = new Set(Object.keys(this.cacheData.usedIds)); const updatedHunks = hunks.map(hunk => { // Generate content-based fingerprint const parsedHunk = { index: hunk.index, header: hunk.header, oldStart: hunk.oldStart, oldLines: hunk.oldLines, newStart: hunk.newStart, newLines: hunk.newLines, changes: hunk.changes, }; const fingerprint = generateContentFingerprint(parsedHunk, filePath); // Check if we already have an ID for this fingerprint if (this.cacheData.fingerprints[fingerprint]) { const cachedId = this.cacheData.fingerprints[fingerprint]; // Update last seen time if (this.cacheData.usedIds[cachedId]) { this.cacheData.usedIds[cachedId].lastSeen = now; } return { ...hunk, id: cachedId }; } // Generate new ID let length = 4; let id = fingerprint.substring(0, length); // Handle collisions by increasing length while (existingIds.has(id) && length < fingerprint.length) { length++; id = fingerprint.substring(0, length); } // Store the mapping this.cacheData.fingerprints[fingerprint] = id; this.cacheData.usedIds[id] = { id, fingerprint, lastSeen: now, }; existingIds.add(id); return { ...hunk, id }; }); this.saveCache(); return updatedHunks; } /** * Find hunk by ID or index */ findHunk(hunks, selector) { if (typeof selector === 'number') { // Find by index (1-based) return hunks.find(h => h.index === selector); } // selector is a string const selectorStr = String(selector).trim(); // First try to find by ID const byId = hunks.find(h => h.id === selectorStr); if (byId) { return byId; } // If not found by ID, try to parse as number for index lookup const num = parseInt(selectorStr, 10); if (!isNaN(num) && String(num) === selectorStr) { // It's a pure number, find by index return hunks.find(h => h.index === num); } // Not found return undefined; } /** * Clear the cache */ clearCache() { this.cacheData = { version: 2, fingerprints: {}, usedIds: {}, history: [] }; this.saveCache(); } /** * Add a staging history entry */ addHistory(entry) { const historyEntry = { id: new Date().toISOString(), timestamp: Date.now(), ...entry, }; if (!this.cacheData.history) { this.cacheData.history = []; } // Add to beginning of array (newest first) this.cacheData.history.unshift(historyEntry); // Keep only last 20 entries if (this.cacheData.history.length > 20) { this.cacheData.history = this.cacheData.history.slice(0, 20); } this.saveCache(); } /** * Get history entries */ getHistory() { return this.cacheData.history || []; } /** * Get specific history entry by index (0 = most recent) */ getHistoryEntry(index) { const history = this.getHistory(); return history[index]; } /** * Remove a history entry */ removeHistoryEntry(index) { const history = this.getHistory(); if (index >= 0 && index < history.length) { history.splice(index, 1); this.saveCache(); } } }