or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

array-diffing.mdcharacter-diffing.mdcss-diffing.mdcustom-diffing.mdformat-conversion.mdindex.mdjson-diffing.mdline-diffing.mdpatch-application.mdpatch-creation.mdpatch-utilities.mdsentence-diffing.mdword-diffing.md
tile.json

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');