Crate mpatch

Crate mpatch 

Source
Expand description

A smart, context-aware patch tool that applies diffs using fuzzy matching.

mpatch is designed to apply unified diffs to a codebase, but with a key difference from the standard patch command: it doesn’t rely on strict line numbers. Instead, it finds the correct location to apply changes by searching for the surrounding context lines.

This makes it highly resilient to patches that are “out of date” because of preceding changes, which is a common scenario when working with AI-generated diffs, code from pull requests, or snippets from documentation.

§Getting Started

The simplest way to use mpatch is the one-shot patch_content_str() function. It’s perfect for the common workflow of taking a diff string (e.g., from an LLM in a markdown file) and applying it to some existing content in memory.

use mpatch::{patch_content_str, ApplyOptions};

// 1. Define the original content and the diff.
let original_content = "fn main() {\n    println!(\"Hello, world!\");\n}\n";
let diff_content = r#"
A markdown file with a diff block.
```diff
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
 fn main() {
-    println!("Hello, world!");
+    println!("Hello, mpatch!");
 }
```
"#;

// 2. Call the one-shot function to parse and apply the patch.
let options = ApplyOptions::new();
let new_content = patch_content_str(diff_content, Some(original_content), &options)?;

// 3. Verify the new content.
let expected_content = "fn main() {\n    println!(\"Hello, mpatch!\");\n}\n";
assert_eq!(new_content, expected_content);

§Applying Patches to Files

For CLI tools or scripts that need to modify files on disk, the workflow involves parsing and then using apply_patches_to_dir(). This example shows the end-to-end process in a temporary directory.

use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions};
use std::fs;
use tempfile::tempdir;

// 1. Set up a temporary directory and a file to be patched.
let dir = tempdir()?;
let file_path = dir.path().join("src/main.rs");
fs::create_dir_all(file_path.parent().unwrap())?;
fs::write(&file_path, "fn main() {\n    println!(\"Hello, world!\");\n}\n")?;

// 2. Define the diff content, as if it came from a markdown file.
let diff_content = r#"
Some introductory text.

```diff
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
 fn main() {
-    println!("Hello, world!");
+    println!("Hello, mpatch!");
 }
```

Some concluding text.
"#;

// 3. Parse the diff content to get patches.
let patches = parse_auto(diff_content)?;

// 4. Apply the patches to the directory.
let options = ApplyOptions::new();
let result = apply_patches_to_dir(&patches, dir.path(), options);

// The batch operation should succeed.
assert!(result.all_succeeded());

// 5. Verify the file was changed correctly.
let new_content = fs::read_to_string(&file_path)?;
let expected_content = "fn main() {\n    println!(\"Hello, mpatch!\");\n}\n";
assert_eq!(new_content, expected_content);

§Key Concepts

§The Patching Workflow

Using the mpatch library typically involves a two-step process: parsing and applying.

§1. Parsing

First, you convert diff text into a structured Vec<Patch>. mpatch provides several functions for this, depending on your input format:

  • parse_auto(): The recommended entry point. It automatically detects the format (Markdown, Unified Diff, or Conflict Markers) and parses the content accordingly.
  • parse_single_patch(): A convenient wrapper around parse_auto() that ensures the input contains exactly one patch, returning a Result<Patch, _>.
  • parse_diffs(): Scans a string for markdown code blocks containing diffs.
  • parse_patches(): A lower-level parser that processes a raw unified diff string directly, without needing markdown fences.
  • parse_conflict_markers(): Parses a string containing conflict markers (<<<<, ====, >>>>) into patches.
  • parse_patches_from_lines(): The lowest-level parser. It operates on an iterator of lines, which is useful for streaming or avoiding large string allocations.

You can also use detect_patch() to identify the format (Markdown, Unified, or Conflict) without parsing the full content.

§2. Applying

Once you have a Patch, you can apply it using one of the apply functions:

  • apply_patches_to_dir(): Applies a list of patches to a directory. This is ideal for processing multi-file diffs.
  • apply_patch_to_file(): The most convenient function for applying a single patch to a file. It handles reading the original file and writing the new content back to disk.
  • apply_patch_to_content(): A pure function for in-memory operations. It takes the original content as a string and returns the new content.

Each of these also has a “strict” try_ variant (e.g., try_apply_patch_to_file()) that treats partial applications as an error, simplifying the common apply-or-fail workflow.

§Core Data Structures

  • Patch: Represents all the changes for a single file. It contains the target file path and a list of hunks.
  • Hunk: Represents a single block of changes within a patch, corresponding to a block of changes (like a @@ ... @@ section in a unified diff). For Conflict Markers, the “before” block is treated as deletions and the “after” block as additions.

§Context-Driven Matching

The core philosophy of mpatch is to ignore strict line numbers. Instead, it searches for the context of a hunk—the lines that are unchanged or being deleted.

  • Primary Search: It first looks for an exact, character-for-character match of the hunk’s context.
  • Ambiguity Resolution: If the same context appears in multiple places, mpatch uses the line numbers (e.g., from the @@ ... @@ header) as a hint to find the most likely location. Note that patches derived from Conflict Markers typically lack line numbers, so ambiguity cannot be resolved this way.
  • Fuzzy Matching: If no exact match is found, it uses a similarity algorithm to find the best fuzzy match, making it resilient to minor changes in the surrounding code.

§Advanced Usage

§Configuring ApplyOptions

The behavior of the apply functions is controlled by the ApplyOptions struct. mpatch provides several convenient ways to construct it:

use mpatch::ApplyOptions;

// For default behavior (fuzzy matching enabled, not a dry run)
let default_options = ApplyOptions::new();

// For common presets
let dry_run_options = ApplyOptions::dry_run();
let exact_options = ApplyOptions::exact();

// For custom configurations using the new fluent methods
let custom_fluent = ApplyOptions::new()
    .with_dry_run(true)
    .with_fuzz_factor(0.9);

// For complex configurations using the builder pattern
let custom_builder = ApplyOptions::builder()
    .dry_run(true)
    .fuzz_factor(0.9)
    .build();

assert_eq!(custom_fluent.dry_run, custom_builder.dry_run);
assert_eq!(custom_fluent.fuzz_factor, custom_builder.fuzz_factor);

§In-Memory Operations and Error Handling

This example demonstrates how to use apply_patch_to_content() for in-memory operations and how to programmatically handle cases where a patch only partially applies.

use mpatch::{parse_single_patch, apply_patch_to_content, HunkApplyError};

// 1. Define original content and a patch where the second hunk will fail.
let original_content = "line 1\nline 2\nline 3\n\nline 5\nline 6\nline 7\n";
let diff_content = r#"
```diff
--- a/partial.txt
+++ b/partial.txt
@@ -1,3 +1,3 @@
 line 1
-line 2
+line two
 line 3
@@ -5,3 +5,3 @@
 line 5
-line WRONG CONTEXT
+line six
 line 7
```
"#;

// 2. Parse the diff.
let patch = parse_single_patch(diff_content)?;

// 3. Apply the patch to the content in memory.
let options = mpatch::ApplyOptions::exact();
let result = apply_patch_to_content(&patch, Some(original_content), &options);

// 4. Verify that the patch did not apply cleanly.
assert!(!result.report.all_applied_cleanly());

// 5. Inspect the specific failures.
let failures = result.report.failures();
assert_eq!(failures.len(), 1);
assert_eq!(failures[0].hunk_index, 2); // Hunk indices are 1-based.
assert!(matches!(failures[0].reason, HunkApplyError::ContextNotFound));

// 6. Verify that the content was still partially modified by the successful first hunk.
let expected_content = "line 1\nline two\nline 3\n\nline 5\nline 6\nline 7\n";
assert_eq!(result.new_content, expected_content);

§Strict Apply-or-Fail Workflow with try_ functions

The previous example showed how to manually check result.report.all_applied_cleanly() to detect partial failures. For workflows where any failed hunk should be treated as a hard error, mpatch provides “strict” variants of the apply functions.

These functions return a Result where a partial application is mapped to a Err(StrictApplyError::PartialApply { .. }). This simplifies the common apply-or-fail pattern.

use mpatch::{parse_single_patch, try_apply_patch_to_content, ApplyOptions, StrictApplyError};

let original_content = "line 1\nline 2\n";
let failing_diff = r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,2 @@
 line 1
-WRONG CONTEXT
+line two
```
"#;
let patch = parse_single_patch(failing_diff)?;
let options = ApplyOptions::exact();

// Using the try_ variant simplifies error handling.
let result = try_apply_patch_to_content(&patch, Some(original_content), &options);

assert!(matches!(result, Err(StrictApplyError::PartialApply { .. })));

§Step-by-Step Application with HunkApplier

For maximum control, you can use the HunkApplier iterator to apply hunks one at a time and inspect the state between each step.

use mpatch::{parse_single_patch, HunkApplier, HunkApplyStatus, ApplyOptions};

// 1. Define original content and a patch.
let original_lines = vec!["line 1", "line 2", "line 3"];
let diff_content = r#"
```diff
--- a/file.txt
+++ b/file.txt
@@ -2,1 +2,1 @@
-line 2
+line two
```
"#;
let patch = parse_single_patch(diff_content)?;
let options = ApplyOptions::new();

// 2. Create the applier.
let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);

// 3. Apply the first (and only) hunk.
let status = applier.next().unwrap();
assert!(matches!(status, HunkApplyStatus::Applied { .. }));

// 4. Check that there are no more hunks.
assert!(applier.next().is_none());

// 5. Finalize the content.
let new_content = applier.into_content();
assert_eq!(new_content, "line 1\nline two\nline 3\n");

§Feature Flags

mpatch includes the following optional features:

§parallel

  • Enabled by default.

  • This feature enables parallel processing for the fuzzy matching algorithm using the rayon crate. When an exact match for a hunk is not found, mpatch performs a computationally intensive search for the best fuzzy match. The parallel feature significantly speeds up this process on multi-core systems by distributing the search across multiple threads.

  • To disable this feature, specify default-features = false in your Cargo.toml:

    [dependencies]
    mpatch = { version = "1.3.3", default-features = false }

    You might want to disable this feature if you are compiling for a target that does not support threading (like wasm32-unknown-unknown) or if you want to minimize dependencies and binary size.

Structs§

ApplyOptions
Options for configuring how a patch is applied.
ApplyOptionsBuilder
Creates a new builder for ApplyOptions.
ApplyResult
Contains detailed results for each hunk within a patch operation.
BatchResult
The result of applying a batch of patches to a directory.
DefaultHunkFinder
The default, built-in strategy for finding hunk locations.
Hunk
Represents a single hunk of changes within a patch.
HunkApplier
An iterator that applies hunks from a patch one by one.
HunkFailure
Details about a hunk that failed to apply.
HunkLocation
Represents the location where a hunk should be applied.
InMemoryResult
The result of an in-memory patch operation.
Patch
Represents all the changes to be applied to a single file.
PatchResult
The result of an apply_patch_to_file() operation.

Enums§

HunkApplyError
The reason a hunk failed to apply.
HunkApplyStatus
The result of applying a single hunk.
MatchType
Describes the method used to successfully locate and apply a hunk.
OneShotError
Represents errors that can occur during the high-level patch_content_str() operation.
ParseError
Represents errors that can occur during the parsing of a diff file.
PatchError
Represents “hard” errors that can occur during patch operations.
PatchFormat
Identifies the syntactic format of a patch content string.
SingleParseError
Represents errors that can occur when parsing a diff expected to contain exactly one patch.
StrictApplyError
Represents errors that can occur during “strict” apply operations.

Traits§

HunkFinder
A trait for strategies that find the location to apply a hunk.

Functions§

apply_hunk_to_lines
Applies a single hunk to a mutable vector of lines in-place.
apply_patch_to_content
Applies the logic of a patch to a string content.
apply_patch_to_file
A convenience function that applies a single Patch to the filesystem.
apply_patch_to_lines
Applies the logic of a patch to a slice of lines.
apply_patches_to_dir
A convenience function that applies a slice of Patch objects to a target directory.
detect_patch
Automatically detects the patch format of the provided content.
ensure_path_is_safe
Ensures a relative path, when joined to a base directory, resolves to a location that is still inside that base directory.
find_hunk_location
Finds the location to apply a hunk to a given text content without modifying it.
find_hunk_location_in_lines
Finds the location to apply a hunk to a slice of lines without modifying it.
parse_auto
Automatically detects the format of the input text and parses it into a list of patches.
parse_conflict_markers
Parses a string containing “Conflict Marker” style diffs (<<<<, ====, >>>>).
parse_diffs
Parses a string containing one or more markdown diff blocks into a vector of Patch objects.
parse_patches
Parses a string containing raw unified diff content into a vector of Patch objects.
parse_patches_from_lines
Parses an iterator of lines containing raw unified diff content into a vector of Patch objects.
parse_single_patch
Parses a string containing a diff and returns a single Patch object.
patch_content_str
A high-level, one-shot function to parse a diff and apply it to a string.
try_apply_patch_to_content
A strict variant of apply_patch_to_content() that treats partial applications as an error.
try_apply_patch_to_file
A strict variant of apply_patch_to_file() that treats partial applications as an error.
try_apply_patch_to_lines
A strict variant of apply_patch_to_lines() that treats partial applications as an error.