UNPKG

git-tweezers

Version:

Advanced git staging tool with hunk and line-level control

152 lines (151 loc) 6.1 kB
export class PatchBuilder { buildPatch(files) { const patches = []; for (const file of files) { // Add file header patches.push(`diff --git a/${file.oldPath} b/${file.newPath}`); patches.push(`index 0000000..0000000 100644`); patches.push(`--- a/${file.oldPath}`); patches.push(`+++ b/${file.newPath}`); // Add hunks for (const hunk of file.hunks) { patches.push(hunk.header); // Process all changes in the hunk for (let i = 0; i < hunk.changes.length; i++) { const change = hunk.changes[i]; patches.push(this.formatLineChange(change)); // Check if we need to add a no-newline marker after this change if (!change.eol) { // Only add the marker if this is not a context line that's followed by more changes const nextChange = hunk.changes[i + 1]; const shouldAddMarker = i === hunk.changes.length - 1 || // Last change in hunk (nextChange && nextChange.type !== 'UnchangedLine'); // Next change is not context if (shouldAddMarker) { patches.push('\\ No newline at end of file'); } } } } } // Join with newlines and add final newline return patches.join('\n') + '\n'; } buildHunkPatch(file, hunkIndex) { if (hunkIndex < 0 || hunkIndex >= file.hunks.length) { return null; } const hunk = file.hunks[hunkIndex]; const fileWithSingleHunk = { ...file, hunks: [hunk], }; return this.buildPatch([fileWithSingleHunk]); } buildLinePatch(file, selectedExtendedLineChanges, originalHunk) { // Rebuild the hunk with only selected changes const rebuiltHunk = this.rebuildHunk(originalHunk, selectedExtendedLineChanges); const fileWithRebuiltHunk = { ...file, hunks: [rebuiltHunk], }; return this.buildPatch([fileWithRebuiltHunk]); } rebuildHunk(originalHunk, selectedExtendedLineChanges) { const newExtendedLineChanges = []; let oldCount = 0; let newCount = 0; // Parse original header to get base line numbers const headerMatch = originalHunk.header.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/); if (!headerMatch) { throw new Error('Invalid hunk header'); } const oldStart = parseInt(headerMatch[1], 10); const newStart = parseInt(headerMatch[3], 10); if (process.env.DEBUG === '1') { console.log(`rebuildHunk: Original hunk has ${originalHunk.changes.length} changes`); console.log(`rebuildHunk: Selected ${selectedExtendedLineChanges.length} changes`); } // Process each change for (const change of originalHunk.changes) { if (selectedExtendedLineChanges.includes(change)) { if (process.env.DEBUG === '1') { console.log(` Including selected: ${change.type} "${change.content}" (eol: ${change.eol})`); } // Keep this change newExtendedLineChanges.push(change); if (change.type === 'DeletedLine') { oldCount++; } else if (change.type === 'AddedLine') { newCount++; } else { oldCount++; newCount++; } } else { // Convert add/delete to context if (change.type === 'AddedLine') { // Skip adds that we don't want to stage if (process.env.DEBUG === '1') { console.log(` Skipping unselected add: "${change.content}"`); } continue; } else if (change.type === 'DeletedLine') { // Convert delete to context if (process.env.DEBUG === '1') { console.log(` Converting delete to context: "${change.content}"`); } newExtendedLineChanges.push({ ...change, type: 'UnchangedLine', }); oldCount++; newCount++; } else { // Keep context lines if (process.env.DEBUG === '1') { console.log(` Keeping context: "${change.content}"`); } newExtendedLineChanges.push(change); oldCount++; newCount++; } } } // Build new header const newHeader = `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`; return { header: newHeader, changes: newExtendedLineChanges, }; } formatLineChange(change) { const prefix = change.type === 'AddedLine' ? '+' : change.type === 'DeletedLine' ? '-' : ' '; // Do NOT add newline here - it will be handled by buildPatch return prefix + change.content; } /** * Calculate hunk header from changes */ calculateHunkHeader(changes, oldStart, newStart) { let oldCount = 0; let newCount = 0; for (const change of changes) { if (change.type === 'DeletedLine') { oldCount++; } else if (change.type === 'AddedLine') { newCount++; } else { oldCount++; newCount++; } } return `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`; } }