CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-diff

A JavaScript text diff implementation based on the Myers algorithm for comparing text at different granularities.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

patch-utilities.mddocs/

Patch Utilities

Parse, manipulate, and convert diff patches between different formats. Essential utilities for working with patch files, merging changes, and transforming patch data structures.

Capabilities

parsePatch Function

Parses a unified diff string into structured patch objects for programmatic manipulation.

/**
 * Parse unified diff string into structured patch objects
 * @param diffStr - Unified diff string content
 * @returns Array of structured patch objects
 */
function parsePatch(diffStr);

reversePatch Function

Reverses a patch by swapping old and new file information and inverting add/remove operations.

/**
 * Reverse a patch (swap old/new and invert operations)
 * @param patch - Structured patch object or array of patches
 * @returns Reversed patch with swapped file references and inverted operations
 */
function reversePatch(patch);

merge Function

Merges two patches that modify the same base content, handling conflicts and overlapping changes.

/**
 * Merge two patches with conflict detection
 * @param mine - First patch (my changes)
 * @param theirs - Second patch (their changes)  
 * @param base - Base content that both patches modify (optional)
 * @returns Merged patch with conflict markers where applicable
 */
function merge(mine, theirs, base);

Usage Examples

Parse and Analyze Patches

import { parsePatch } from "diff";

// Parse a patch file
const patchContent = `--- file1.txt
+++ file1.txt
@@ -1,3 +1,3 @@
 line 1
-line 2
+modified line 2
 line 3

--- file2.txt
+++ file2.txt
@@ -1,2 +1,3 @@
 existing line
+new line
 another line`;

const patches = parsePatch(patchContent);
console.log(`Found ${patches.length} file patches`);

patches.forEach((patch, index) => {
  console.log(`\nPatch ${index + 1}:`);
  console.log(`  File: ${patch.oldFileName} -> ${patch.newFileName}`);
  console.log(`  Hunks: ${patch.hunks.length}`);
  
  patch.hunks.forEach((hunk, hunkIndex) => {
    console.log(`  Hunk ${hunkIndex + 1}: lines ${hunk.oldStart}-${hunk.oldStart + hunk.oldLines - 1}`);
    console.log(`    Changes: ${hunk.lines.filter(l => l.startsWith('+') || l.startsWith('-')).length}`);
  });
});

Reverse Patches

import { reversePatch, parsePatch, formatPatch } from "diff";

// Create a reverse patch to undo changes
const originalPatch = parsePatch(patchString)[0];
const undoPatch = reversePatch(originalPatch);

console.log("Original patch changes file from A to B");
console.log("Reverse patch changes file from B to A");

// Format back to string
const undoPatchString = formatPatch(undoPatch);
console.log("Undo patch:\n", undoPatchString);

// Reverse multiple patches
const multiplePatches = parsePatch(multiFilePatchString);
const reversedPatches = reversePatch(multiplePatches);

Merge Patches

import { merge, parsePatch } from "diff";

// Two developers made different changes to the same file
const myPatch = `--- config.json
+++ config.json
@@ -1,4 +1,4 @@
 {
   "version": "1.0",
-  "debug": false,
+  "debug": true,
   "features": []
 }`;

const theirPatch = `--- config.json
+++ config.json
@@ -1,4 +1,5 @@
 {
   "version": "1.0",
   "debug": false,
+  "newFeature": "enabled",
   "features": []
 }`;

const myParsed = parsePatch(myPatch)[0];
const theirParsed = parsePatch(theirPatch)[0];

// Merge the patches
const merged = merge(myParsed, theirParsed);

if (merged.conflict) {
  console.log("Merge has conflicts that need manual resolution");
} else {
  console.log("Patches merged successfully");
}

console.log("Merged hunks:", merged.hunks.length);

Advanced Usage

Patch Analysis and Statistics

import { parsePatch } from "diff";

function analyzePatchFile(patchContent) {
  const patches = parsePatch(patchContent);
  
  const analysis = {
    files: patches.length,
    totalHunks: 0,
    linesAdded: 0,
    linesRemoved: 0,
    linesContext: 0,
    fileTypes: {},
    largestHunk: 0,
    conflictingFiles: []
  };
  
  patches.forEach(patch => {
    analysis.totalHunks += patch.hunks.length;
    
    // Analyze file type
    const extension = patch.oldFileName.split('.').pop() || 'no-extension';
    analysis.fileTypes[extension] = (analysis.fileTypes[extension] || 0) + 1;
    
    patch.hunks.forEach(hunk => {
      const hunkSize = hunk.lines.length;
      analysis.largestHunk = Math.max(analysis.largestHunk, hunkSize);
      
      hunk.lines.forEach(line => {
        if (line.startsWith('+')) {
          analysis.linesAdded++;
        } else if (line.startsWith('-')) {
          analysis.linesRemoved++;
        } else if (line.startsWith(' ')) {
          analysis.linesContext++;
        }
      });
    });
    
    // Check for potential conflicts (placeholder logic)
    if (patch.hunks.some(h => h.lines.length > 50)) {
      analysis.conflictingFiles.push(patch.oldFileName);
    }
  });
  
  return analysis;
}

// Usage
const patchAnalysis = analyzePatchFile(largePatchString);
console.log(`Patch affects ${patchAnalysis.files} files`);
console.log(`Changes: +${patchAnalysis.linesAdded} -${patchAnalysis.linesRemoved}`);
console.log(`File types:`, patchAnalysis.fileTypes);

Patch Transformation

import { parsePatch, formatPatch } from "diff";

function transformPatch(patchContent, transformer) {
  const patches = parsePatch(patchContent);
  
  const transformed = patches.map(patch => ({
    ...patch,
    oldFileName: transformer.transformPath(patch.oldFileName),
    newFileName: transformer.transformPath(patch.newFileName),
    hunks: patch.hunks.map(hunk => ({
      ...hunk,
      lines: hunk.lines.map(line => transformer.transformLine(line))
    }))
  }));
  
  return formatPatch(transformed);
}

// Example transformer: update import paths
const pathUpdateTransformer = {
  transformPath: (path) => path.replace(/^src\//, 'lib/'),
  transformLine: (line) => {
    if (line.startsWith('+') || line.startsWith('-')) {
      const prefix = line[0];
      const content = line.slice(1);
      const updated = content.replace(/import.*from ['"]\.\.\/src\//g, 
        match => match.replace('../src/', '../lib/'));
      return prefix + updated;
    }
    return line;
  }
};

const updatedPatch = transformPatch(originalPatch, pathUpdateTransformer);

Conflict Resolution

import { merge, parsePatch } from "diff";

function resolveConflicts(mergedPatch, resolutionStrategy) {
  if (!mergedPatch.conflict) {
    return mergedPatch;
  }
  
  const resolved = { ...mergedPatch };
  
  resolved.hunks = mergedPatch.hunks.map(hunk => {
    if (!hunk.conflict) {
      return hunk;
    }
    
    const resolvedLines = [];
    
    hunk.lines.forEach(line => {
      if (line.conflict) {
        // Apply resolution strategy
        switch (resolutionStrategy) {
          case 'mine':
            resolvedLines.push(...line.mine);
            break;
          case 'theirs':
            resolvedLines.push(...line.theirs);
            break;
          case 'both':
            resolvedLines.push(...line.mine);
            resolvedLines.push(...line.theirs);
            break;
          case 'markers':
            resolvedLines.push('<<<<<<< mine');
            resolvedLines.push(...line.mine);
            resolvedLines.push('=======');
            resolvedLines.push(...line.theirs);
            resolvedLines.push('>>>>>>> theirs');
            break;
        }
      } else {
        resolvedLines.push(line);
      }
    });
    
    return {
      ...hunk,
      lines: resolvedLines,
      conflict: false
    };
  });
  
  resolved.conflict = false;
  return resolved;
}

// Usage
const conflictedMerge = merge(patch1, patch2);
if (conflictedMerge.conflict) {
  const resolvedWithMarkers = resolveConflicts(conflictedMerge, 'markers');
  const resolvedMine = resolveConflicts(conflictedMerge, 'mine');
  const resolvedTheirs = resolveConflicts(conflictedMerge, 'theirs');
}

Patch Validation

import { parsePatch } from "diff";

function validatePatch(patchContent) {
  const validation = {
    isValid: true,
    errors: [],
    warnings: [],
    patches: []
  };
  
  try {
    const patches = parsePatch(patchContent);
    validation.patches = patches;
    
    patches.forEach((patch, patchIndex) => {
      // Check for missing file names
      if (!patch.oldFileName || !patch.newFileName) {
        validation.errors.push(`Patch ${patchIndex}: Missing file names`);
        validation.isValid = false;
      }
      
      // Validate hunks
      patch.hunks.forEach((hunk, hunkIndex) => {
        // Check hunk integrity
        const addLines = hunk.lines.filter(l => l.startsWith('+')).length;
        const removeLines = hunk.lines.filter(l => l.startsWith('-')).length;
        const contextLines = hunk.lines.filter(l => l.startsWith(' ')).length;
        
        const expectedOldLines = removeLines + contextLines;
        const expectedNewLines = addLines + contextLines;
        
        if (hunk.oldLines !== expectedOldLines) {
          validation.warnings.push(
            `Patch ${patchIndex}, hunk ${hunkIndex}: Old line count mismatch`
          );
        }
        
        if (hunk.newLines !== expectedNewLines) {
          validation.warnings.push(
            `Patch ${patchIndex}, hunk ${hunkIndex}: New line count mismatch`
          );
        }
        
        // Check for overlapping hunks
        if (hunkIndex > 0) {
          const prevHunk = patch.hunks[hunkIndex - 1];
          if (hunk.oldStart <= prevHunk.oldStart + prevHunk.oldLines) {
            validation.errors.push(
              `Patch ${patchIndex}: Overlapping hunks ${hunkIndex - 1} and ${hunkIndex}`
            );
            validation.isValid = false;
          }
        }
      });
    });
    
  } catch (error) {
    validation.isValid = false;
    validation.errors.push(`Parse error: ${error.message}`);
  }
  
  return validation;
}

// Usage
const validation = validatePatch(suspiciousPatchContent);
if (!validation.isValid) {
  console.error("Invalid patch:", validation.errors);
} else if (validation.warnings.length > 0) {
  console.warn("Patch warnings:", validation.warnings);
} else {
  console.log("Patch is valid");
}

Patch Conversion and Export

import { parsePatch, formatPatch } from "diff";

function convertPatchFormat(patchContent, targetFormat) {
  const patches = parsePatch(patchContent);
  
  switch (targetFormat) {
    case 'git':
      return formatPatch(patches.map(patch => ({
        ...patch,
        oldFileName: `a/${patch.oldFileName}`,
        newFileName: `b/${patch.newFileName}`
      })));
      
    case 'svn':
      // SVN-style format conversion
      return patches.map(patch => {
        const lines = [`Index: ${patch.oldFileName}`, '==================================================================='];
        lines.push(...formatPatch(patch).split('\n').slice(2)); // Skip standard headers
        return lines.join('\n');
      }).join('\n\n');
      
    case 'context':
      // Convert to context diff format (simplified)
      return patches.map(patch => 
        `*** ${patch.oldFileName}\n--- ${patch.newFileName}\n${formatContextDiff(patch)}`
      ).join('\n\n');
      
    default:
      return formatPatch(patches);
  }
}

function formatContextDiff(patch) {
  // Simplified context diff format
  return patch.hunks.map(hunk => {
    const contextLines = [`***************`];
    contextLines.push(`*** ${hunk.oldStart},${hunk.oldStart + hunk.oldLines - 1} ****`);
    
    // Add old lines
    hunk.lines.forEach(line => {
      if (line.startsWith('-') || line.startsWith(' ')) {
        contextLines.push(line.startsWith('-') ? `- ${line.slice(1)}` : `  ${line.slice(1)}`);
      }
    });
    
    contextLines.push(`--- ${hunk.newStart},${hunk.newStart + hunk.newLines - 1} ----`);
    
    // Add new lines
    hunk.lines.forEach(line => {
      if (line.startsWith('+') || line.startsWith(' ')) {
        contextLines.push(line.startsWith('+') ? `+ ${line.slice(1)}` : `  ${line.slice(1)}`);
      }
    });
    
    return contextLines.join('\n');
  }).join('\n');
}

// Usage
const gitPatch = convertPatchFormat(standardPatch, 'git');
const svnPatch = convertPatchFormat(standardPatch, 'svn');

Install with Tessl CLI

npx tessl i tessl/npm-diff

docs

array-diffing.md

character-diffing.md

css-diffing.md

custom-diffing.md

format-conversion.md

index.md

json-diffing.md

line-diffing.md

patch-application.md

patch-creation.md

patch-utilities.md

sentence-diffing.md

word-diffing.md

tile.json