mpatch is a contextual patching program/library tailored towards the "messy" world of modern software development. Unlike standard patch and git apply, which require line numbers and context to be exact byte-for-byte matches, mpatch utilizes fuzzy matching, looking for patches based on the context around the change in the code.
This tool was created to easily apply diffs produced by LLMs (ChatGPT, Gemini, Claude, Copilot), since they tend to hallucinate the exact line numbers or the context around the patch.
🐍 Python Developer? We have official Python bindings! Check out the
mpatchPyPI package and Python documentation.
You ask an AI to modify some code. You get the diff. Except, the comment inside this code block is missing.
Standard patch: Won't work. The context isn't exact byte-for-byte match.
mpatch: Works! It notices that the code structure hasn't changed and applies the patch.
| Original File (Modified Locally) | AI-Generated Patch (Stale Context) | patch Result |
mpatch Result |
|---|---|---|---|
fn main() { |
fn main() { |
❌ Failed |
fn main() { |
mpatch automatically detects the input format. Flags are not needed.
- Code Blocks in Markdown: The default output format for language models. Uses either
```diff,```rust, or other code blocks if they have diff headers. - Unified Diff: Default
git diffordiff -uformat. - Aider Search/Replace Blocks: Search and replace blocks with
<<<<<<< SEARCH(orORIGINAL),=======, and>>>>>>> REPLACE(orUPDATED). File paths are extracted automatically from the block header, preceding markdown text, or code comments. Supports wildcard ellipsis matching (...) to preserve intermediate code. - Conflict Markers: Git-like conflict markers with
<<<<,====, and>>>>. Defaults topatch_targeton the filesystem, or patches strings directly in memory.
- 🧠 Fuzzy Matching: Runs a similarity algorithm to find the most suitable place to apply a patch if no exact matching is found. It supports stale context, whitespace changes, and minor code differences.
- 🔒 Atomic (All-or-Nothing) Mode: Pass
-a/--atomic(or--all-or-nothing) to guarantee that changes are only written to disk if every single patch and hunk succeeds cleanly. If any hunk fails, the filesystem remains completely untouched. - 🤖 Format Independent: Automatically recognizes and processes:
- Markdown diff code blocks (standard chat output format).
- Unified Diff (output from
git diffordiff -ucommand). - Aider Search/Replace blocks (popularized by Aider and LLM coding assistants).
- Conflict Markers (
<<<<,====,>>>>).
- 🔍 Wildcard / Ellipsis Matching: Supports
...,…, and comment-wrapped ellipsis lines (// ... existing code ...,<!-- ... -->, etc.) in search/replace blocks, preserving untouched code gaps while strictly preventing runaway gaps across functions. - 🔀 Three-Way Line Merge: Built-in line-level 3-way merge engine (
merge_three_way) with standard Diff3 conflict markers. - 💡 Sub-Line & Near-Miss Diagnostics: Inline word-level diff visualization (
format_inline_diff) for failed hunks and fuzzy file path suggestions (suggest_close_file_paths) when a target file is missing. - 📥 Standard Input (Stdin): Pipe diffs directly from
git diff,cat, orcurlusing-(e.g.,git diff | mpatch - ./srcorcat patch.diff | mpatch -). - 📋 Clipboard Support: Input directly from your clipboard with
-cor--clipboard. - ✨ Smarter Indentation: Automatically indents added lines to be consistent with the target file. It translates tabs/spaces dynamically and preserves the target file's indentation style.
- 🗑️ File Deletion: Automatically removes the target file if the output becomes empty after patching.
- 🛡️ Secure: Path traversal is automatically prevented, ensuring no patch files can access or overwrite files outside the target directory.
- ⚡ Fast & Concurrent: Parallelizes fuzzy matching across all CPU cores via
rayon, using histogram-based word diffing, zero-allocation window length iteration, and mathematical upper-bound pruning for rapid search. - 🔍 Dry Run: Preview what the tool would do without modifying any files using
--dry-run.
We provide pre-compiled binaries for Windows, macOS, and Linux.
Using cargo-binstall (Fastest):
cargo binstall mpatchManual Download:
- Go to the Releases Page.
- Download the archive for your architecture (see table below).
- Extract and add to your
PATH.
| Platform | Architecture | Target | Notes |
|---|---|---|---|
| macOS | Universal | universal-apple-darwin |
Best for Mac. Runs natively on M1/M2/M3 & Intel. |
| x64 | x86_64-apple-darwin |
Older Intel Macs. | |
| ARM64 | aarch64-apple-darwin |
Apple Silicon (M1/M2/M3). | |
| Windows | x64 | x86_64-pc-windows-msvc |
Standard 64-bit Windows. |
| ARM64 | aarch64-pc-windows-msvc |
Surface Pro X, Parallels. | |
| Linux | x64 | x86_64-unknown-linux-gnu |
Ubuntu, Debian, Fedora, etc. |
| x64 (Static) | x86_64-unknown-linux-musl |
Alpine Linux, Docker containers. | |
| ARM64 | aarch64-unknown-linux-gnu |
Raspberry Pi 4/5, AWS Graviton. | |
| ARM64 (Static) | aarch64-unknown-linux-musl |
Alpine on ARM64. | |
| ARMv7 | armv7-unknown-linux-gnueabihf |
Older Raspberry Pi (2/3), IoT. | |
| ARMv7 (Static) | armv7-unknown-linux-musleabihf |
Static binaries for ARMv7. |
The integrity of all released binaries is verified with GPG. The .sig signature file along with our public key (public.key) will help you verify the authenticity of the downloaded files.
-
Import the public key:
gpg --import public.key
-
Verify the archive:
# Example for Linux x64 gpg --verify mpatch-x86_64-unknown-linux-gnu-v1.7.0.tar.gz.sig mpatch-x86_64-unknown-linux-gnu-v1.7.0.tar.gz
cargo install mpatchApply a patch file (Markdown, Diff, or Conflict markers) to a target directory (defaults to current directory . if omitted).
mpatch changes.md ./src
# Target directory defaults to '.' when omitted:
mpatch changes.mdOnly modify files on disk if all patches and hunks apply cleanly. If any hunk fails, zero files on disk are touched.
mpatch -a changes.md ./srcPipe diffs directly from git diff, cat, or curl using -:
# Pipe from git diff
git diff | mpatch - ./src
# Reverse a piped patch
git diff | mpatch -R - ./src
# Pipe from a file or network stream
cat patch.diff | mpatch - ./src
curl -s https://example.com/fix.patch | mpatch - ./src
# Target directory defaults to current directory ('.') if omitted
cat patch.diff | mpatch -Apply a patch copied to your clipboard directly to a target directory.
mpatch -c ./srcSee exactly what will happen without modifying files.
mpatch --dry-run changes.md ./srcThe default fuzz factor is 0.7. Lower it to accept fewer approximate matches, or raise it when the patch context has drifted. Set it to 0 to disable fuzzy matching; exact matching and a trailing-whitespace-insensitive exact pass are still used.
# Stricter (needs 90% similarity)
mpatch --fuzz-factor 0.9 changes.md ./src
# Disable fuzzy matching (exact matching only, with trailing-whitespace tolerance)
mpatch --fuzz-factor 0.0 changes.md ./srcFor unified diffs whose full hunk context is present, use --fuzz-factor 0 to avoid approximate relocation. It may not work when context has drifted, and does not guarantee identical behavior to the traditional patch command. Preview the result with --dry-run first.
Undo a previously applied patch (swaps additions and deletions).
mpatch -R changes.md ./srcIf a patch fails, generate a comprehensive debug report (includes file states, logs, and diffs) to analyze why.
mpatch -vvvv changes.md ./src
# Generates: mpatch-debug-report-[timestamp].mdmpatch is designed to be the patching engine for AI coding agents and tools. It exposes a robust Rust API.
Add to Cargo.toml:
[dependencies]
mpatch = "1.7.0"Ideal for processing text in memory.
use mpatch::{patch_content_str, ApplyOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let original_code = "fn main() { println!(\"Old\"); }\n";
// Input can be Markdown, Raw Diff, Aider blocks, or Conflict Markers
let patch_text = r#"```diff
--- a/main.rs
+++ b/main.rs
@@ -1 +1 @@
-fn main() { println!("Old"); }
+fn main() { println!("New"); }
```
"#;
let options = ApplyOptions::new(); // Default fuzz_factor: 0.7
let new_code = patch_content_str(patch_text, Some(original_code), &options)?;
assert_eq!(new_code, "fn main() { println!(\"New\"); }\n");
Ok(())
}Ideal for CLI tools applying multi-file patches.
use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let diff_content = r#"```diff
--- a/file1.txt
+++ b/file1.txt
@@ -1 +1 @@
-foo
+bar
--- a/file2.txt
+++ b/file2.txt
@@ -1 +1 @@
-baz
+qux
```
"#;
// 1. Parse (automatically detects format)
let patches = parse_auto(diff_content)?;
// 2. Apply
let target_dir = Path::new("./src");
let options = ApplyOptions::new();
let results = apply_patches_to_dir(&patches, target_dir, options);
if results.all_succeeded() {
println!("All patches applied successfully!");
} else {
// Inspect specific failures
for (path, result) in results.hard_failures() {
eprintln!("Failed to process {}: {}", path.display(), result);
}
}
Ok(())
}Programmatically invert patches (additions become deletions and vice versa).
use mpatch::{invert_patches, parse_auto};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let diff_content = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new\n";
let patches = parse_auto(diff_content)?;
let reversed = invert_patches(&patches);
// Now apply `reversed` to undo changes
assert_eq!(reversed.len(), 1);
Ok(())
}Consolidate multiple patch sections or blocks targeting the same file into unified Patch objects with combined hunks:
use mpatch::{merge_patches, parse_auto};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let diff1 = "--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old1\n+new1\n";
let diff2 = "--- a/file.txt\n+++ b/file.txt\n@@ -10 +10 @@\n-old2\n+new2\n";
let mut patches = parse_auto(diff1)?;
patches.extend(parse_auto(diff2)?);
// Automatically merges patches targeting the same file in O(N) time
let consolidated = merge_patches(patches);
assert_eq!(consolidated.len(), 1);
assert_eq!(consolidated[0].hunks.len(), 2);
Ok(())
}If you want to treat partial applications (where some hunks fail) as an error, use the try_ variants.
use mpatch::{parse_single_patch, try_apply_patch_to_content, ApplyOptions, StrictApplyError};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let original_content = "fn main() { println!(\"Old\"); }\n";
let diff_content = "--- a/main.rs\n+++ b/main.rs\n@@ -1 +1 @@\n-fn main() { println!(\"Old\"); }\n+fn main() { println!(\"New\"); }\n";
let patch = parse_single_patch(diff_content)?;
let options = ApplyOptions::exact();
// Returns an Err if any hunk fails to apply
match try_apply_patch_to_content(&patch, Some(original_content), &options) {
Ok(result) => println!("Success: {}", result.new_content),
Err(StrictApplyError::PartialApply { report }) => {
eprintln!("Patch partially applied. {} hunks failed.", report.failure_count());
}
Err(e) => eprintln!("Hard error: {}", e),
}
Ok(())
}You can also use mpatch to generate patches by comparing two strings.
use mpatch::Patch;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let old_text = "fn main() { println!(\"Old\"); }";
let new_text = "fn main() { println!(\"New\"); }";
// Create a patch with 3 lines of context
let patch = Patch::from_texts("src/main.rs", old_text, new_text, 3)?;
println!("{}", patch);
Ok(())
}Stage all changes in memory and commit to disk if and only if all hunks across all patches apply cleanly:
use mpatch::{parse_auto, apply_patches_to_dir_atomic, ApplyOptions};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let diff_content = "--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new\n";
let patches = parse_auto(diff_content)?;
let target_dir = Path::new("./src");
let options = ApplyOptions::exact();
// Changes are only written to disk if ALL patches and hunks succeed
let batch = apply_patches_to_dir_atomic(&patches, target_dir, options);
if batch.all_applied_cleanly() {
println!("All files updated cleanly on disk!");
} else {
eprintln!("Atomic batch failed: zero files on disk were modified.");
}
Ok(())
}Apply Aider search/replace blocks containing wildcard ellipsis lines (...) that match and preserve multi-line code gaps:
use mpatch::{patch_content_str, ApplyOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let original = "def compute(x):\n setup()\n res = x * 2\n teardown()\n return res\n";
let aider_diff = r#"math.py
<<<<<<< SEARCH
def compute(x):
...
res = x * 2
=======
def compute(x):
...
res = x * 4
>>>>>>> REPLACE
"#;
let options = ApplyOptions::new();
let new_code = patch_content_str(aider_diff, Some(original), &options)?;
assert!(new_code.contains("res = x * 4"));
assert!(new_code.contains("setup()"));
assert!(new_code.contains("teardown()"));
Ok(())
}Perform line-level 3-way merges backed by similar::TextMerge, automatically generating Diff3 conflict markers on conflicts:
use mpatch::merge_three_way;
fn main() {
let base = "Apples\nBananas\nCherries\nDates\n";
let ours = "Apples\nBlueberries\nCherries\nDates\n";
let theirs = "Apples\nBananas\nCherries\nDragonfruit\n";
let (merged, is_conflicted) = merge_three_way(base, ours, theirs, None);
assert!(!is_conflicted);
assert_eq!(merged, "Apples\nBlueberries\nCherries\nDragonfruit\n");
}Visualize sub-line changes with word-level highlights and suggest close matching file paths when a target is not found:
use mpatch::{format_inline_diff, suggest_close_file_paths};
use std::path::Path;
// Highlight sub-line word additions and deletions
fn main() {
// Highlight sub-line word additions and deletions
let expected = vec!["fn compute(x: i32) -> i32 {"];
let actual = vec!["fn compute(x: i64) -> i32 {"];
let diff_view = format_inline_diff(&expected, &actual);
println!("{}", diff_view);
// Find close file path candidates when a patch targets a misspelled path
// Find close file path candidates when a patch targets a misspelled path
let suggestions = suggest_close_file_paths(Path::new("calculate.rs"), Path::new("./src"), 3);
for candidate in suggestions {
println!("Did you mean: {}", candidate.display());
}
}Though supported by mpatch, this file format (<<<<, ====, >>>>) does not include any file path information.
- Using
mpatchfrom CLI: If the provided file contains only conflict markers,mpatchwill try to patch a file calledpatch_target. - Using
mpatchas a library: The format can be used by thepatch_content_strfunction if your target file content is stored in memory.
In case of multiple file patching or using mpatch from the CLI, it is recommended to use the Unified Diffs format (with --- and +++) or Aider Search/Replace blocks.
Fuzzy matching is an
- Heuristics: Before doing a fuzzy match,
mpatchtries to find both exact and "whitespace-insensitive" exact matches. - Anchoring:
mpatchtries to look for unique lines in the patch file to shrink the match range. - Parallelism: If a full scan is required, it uses Rayon to parallelize the workload.
Benchmark code can be found in benches/mpatch_bench.rs. To run, use cargo bench.
Contributions are welcome!
- Bug Reports: Please run
mpatch -vvvv ...to generate a debug report and attach it to your issue. - Development:
git clone https://github.com/romelium/mpatch.git cd mpatch cargo test
MIT License. See LICENSE for details.