All files / src/cli diff-apply.ts

100% Statements 36/36
92.85% Branches 13/14
100% Functions 3/3
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84                        23x 23x 23x   23x 63x 63x   63x   22x 22x   25x 25x       16x 16x 16x           23x   23x 38x 38x 1x   37x 4x             18x 18x               18x 2x     16x 16x 2x     14x 14x     14x   14x 16x     11x    
/**
 * Pure diff application functions.
 * Apply unified diffs to file content without side effects beyond file I/O.
 */
 
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { parsePatch, type DiffHunk } from '../diff/index.js';
 
/**
 * Apply a single hunk to the file content lines.
 */
export function applyHunk(lines: string[], hunk: DiffHunk): string[] {
  const result = [...lines];
  const oldLines: string[] = [];
  const newLines: string[] = [];
 
  for (const line of hunk.lines) {
    const prefix = line[0] ?? '';
    const content = line.slice(1);
 
    switch (prefix) {
      case '-':
        oldLines.push(content);
        break;
      case '+':
        newLines.push(content);
        break;
      case ' ':
      case '':
        // Context line appears in both old and new
        oldLines.push(content);
        newLines.push(content);
        break;
    }
  }
 
  // Convert 1-based line number to 0-based index.
  // New file diffs use @@ -0,0 +1,N @@, so clamp to 0.
  const startIndex = Math.max(0, hunk.oldStart - 1);
 
  for (let i = 0; i < oldLines.length; i++) {
    const lineIndex = startIndex + i;
    if (lineIndex >= result.length) {
      throw new Error(`Hunk context mismatch: line ${lineIndex + 1} doesn't exist`);
    }
    if (result[lineIndex] !== oldLines[i]) {
      throw new Error(
        `Hunk context mismatch at line ${lineIndex + 1}: ` +
          `expected "${oldLines[i]}", got "${result[lineIndex]}"`
      );
    }
  }
 
  result.splice(startIndex, oldLines.length, ...newLines);
  return result;
}
 
/**
 * Apply a unified diff to a file.
 * Hunks are applied in reverse order by line number to prevent line shift issues.
 */
export function applyUnifiedDiff(filePath: string, diff: string): void {
  if (!existsSync(filePath)) {
    throw new Error(`File not found: ${filePath}`);
  }
 
  const hunks = parsePatch(diff);
  if (hunks.length === 0) {
    throw new Error('No valid hunks found in diff');
  }
 
  const content = readFileSync(filePath, 'utf-8');
  let lines = content.split('\n');
 
  // Sort hunks by oldStart in descending order to apply from bottom to top
  const sortedHunks = [...hunks].sort((a, b) => b.oldStart - a.oldStart);
 
  for (const hunk of sortedHunks) {
    lines = applyHunk(lines, hunk);
  }
 
  writeFileSync(filePath, lines.join('\n'));
}