mpatch/lib.rs
1//! A smart, context-aware patch tool that applies diffs using fuzzy matching.
2//!
3//! `mpatch` is designed to apply unified diffs to a codebase, but with a key
4//! difference from the standard `patch` command: it doesn't rely on strict line
5//! numbers. Instead, it finds the correct location to apply changes by searching
6//! for the surrounding context lines.
7//!
8//! This makes it highly resilient to patches that are "out of date" because of
9//! preceding changes, which is a common scenario when working with AI-generated
10//! diffs, code from pull requests, or snippets from documentation.
11//!
12//! ## Why mpatch?
13//!
14//! Standard patching tools are fragile. They rely on exact line numbers and
15//! byte-for-byte context matches. In a modern workflow involving LLMs, code
16//! often drifts quickly. `mpatch` solves this by:
17//!
18//! - **Flexible Anchoring**: Searching for the best fit in a file, even if the
19//! target has moved dozens of lines.
20//! - **Fuzzy Similarity**: Accepting matches that are "close enough" (e.g.,
21//! modified comments or minor whitespace changes).
22//! - **Indentation Awareness**: Dynamically re-aligning the indentation of
23//! injected code to match the target file's style.
24//!
25//! ## Format Support & Limitations
26//!
27//! `mpatch` handles Unified Diffs and Markdown blocks natively. It also supports
28//! **Conflict Markers** (`<<<<`, `====`, `>>>>`), but with a significant caveat:
29//! conflict markers do not encode the target file path. When parsed, they default
30//! to a placeholder path (`patch_target`).
31//!
32//! ## Getting Started
33//!
34//! The simplest way to use `mpatch` is the one-shot [`patch_content_str()`] function.
35//! It's perfect for the common workflow of taking a diff string (e.g., from an
36//! LLM in a markdown file) and applying it to some existing content in memory.
37//!
38//! ````rust
39//! use mpatch::{patch_content_str, ApplyOptions};
40//!
41//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
42//! // 1. Define the original content and the diff.
43//! let original_content = "fn main() {\n println!(\"Hello, world!\");\n}\n";
44//! let diff_content = r#"
45//! A markdown file with a diff block.
46//! ```diff
47//! --- a/src/main.rs
48//! +++ b/src/main.rs
49//! @@ -1,3 +1,3 @@
50//! fn main() {
51//! - println!("Hello, world!");
52//! + println!("Hello, mpatch!");
53//! }
54//! ```
55//! "#;
56//!
57//! // 2. Call the one-shot function to parse and apply the patch.
58//! let options = ApplyOptions::new();
59//! let new_content = patch_content_str(diff_content, Some(original_content), &options)?;
60//!
61//! // 3. Verify the new content.
62//! let expected_content = "fn main() {\n println!(\"Hello, mpatch!\");\n}\n";
63//! assert_eq!(new_content, expected_content);
64//!
65//! # Ok(())
66//! # }
67//! ````
68//!
69//! ## Applying Patches to Files
70//!
71//! For CLI tools or scripts that need to modify files on disk, the workflow involves
72//! parsing and then using [`apply_patches_to_dir()`]. This example shows the end-to-end
73//! process in a temporary directory.
74//!
75//! ````rust
76//! use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions};
77//! use std::fs;
78//! use tempfile::tempdir;
79//!
80//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
81//! // 1. Set up a temporary directory and a file to be patched.
82//! let dir = tempdir()?;
83//! let file_path = dir.path().join("src/main.rs");
84//! fs::create_dir_all(file_path.parent().unwrap())?;
85//! fs::write(&file_path, "fn main() {\n println!(\"Hello, world!\");\n}\n")?;
86//!
87//! // 2. Define the diff content, as if it came from a markdown file.
88//! let diff_content = r#"
89//! Some introductory text.
90//!
91//! ```diff
92//! --- a/src/main.rs
93//! +++ b/src/main.rs
94//! @@ -1,3 +1,3 @@
95//! fn main() {
96//! - println!("Hello, world!");
97//! + println!("Hello, mpatch!");
98//! }
99//! ```
100//!
101//! Some concluding text.
102//! "#;
103//!
104//! // 3. Parse the diff content to get patches.
105//! let patches = parse_auto(diff_content)?;
106//!
107//! // 4. Apply the patches to the directory.
108//! let options = ApplyOptions::new();
109//! let result = apply_patches_to_dir(&patches, dir.path(), options);
110//!
111//! // The batch operation should succeed.
112//! assert!(result.all_succeeded());
113//!
114//! // 5. Verify the file was changed correctly.
115//! let new_content = fs::read_to_string(&file_path)?;
116//! let expected_content = "fn main() {\n println!(\"Hello, mpatch!\");\n}\n";
117//! assert_eq!(new_content, expected_content);
118//! # Ok(())
119//! # }
120//! ````
121//!
122//! ## Key Concepts
123//!
124//! ### The Patching Workflow
125//!
126//! Using the `mpatch` library typically involves a two-step process: parsing and applying.
127//!
128//! #### 1. Parsing
129//!
130//! First, you convert diff text into a structured `Vec<Patch>`. `mpatch` provides
131//! several functions for this, depending on your input format:
132//!
133//! - [`parse_auto()`]: The recommended entry point. It automatically detects the format
134//! (Markdown, Unified Diff, or Conflict Markers) and parses the content accordingly.
135//! - [`parse_single_patch()`]: A convenient wrapper around `parse_auto()` that ensures
136//! the input contains exactly one patch, returning a `Result<Patch, _>`.
137//! - [`parse_diffs()`]: Scans a string for markdown code blocks containing diffs.
138//! - [`parse_patches()`]: A lower-level parser that processes a raw unified diff string
139//! directly, without needing markdown fences.
140//! - [`parse_conflict_markers()`]: Parses a string containing conflict markers
141//! (`<<<<`, `====`, `>>>>`) into patches.
142//! - [`parse_patches_from_lines()`]: The lowest-level parser. It operates on an iterator
143//! of lines, which is useful for streaming or avoiding large string allocations.
144//!
145//! You can also use [`detect_patch()`] to identify the format (Markdown, Unified, or Conflict)
146//! without parsing the full content.
147//!
148//! #### 2. Applying
149//!
150//! Once you have a `Patch`, you can apply it using one of the `apply` functions:
151//!
152//! - [`apply_patches_to_dir()`]: Applies a list of patches to a directory. This is
153//! ideal for processing multi-file diffs.
154//! - [`apply_patch_to_file()`]: The most convenient function for applying a single
155//! patch to a file. It handles reading the original file and writing the new content
156//! back to disk. If the patch results in empty content, the file is deleted.
157//! - [`apply_patch_to_content()`]: A pure function for in-memory operations. It takes
158//! the original content as a string and returns the new content.
159//! - [`apply_patch_to_lines()`]: Similar to `apply_patch_to_content()`, but operates
160//! directly on a slice of lines, avoiding string allocations.
161//!
162//! Each of these also has a "strict" `try_` variant (e.g., [`try_apply_patch_to_file()`])
163//! that treats partial applications as an error, simplifying the common apply-or-fail
164//! workflow.
165//!
166//! You can also manipulate patches before application:
167//!
168//! - [`invert_patches()`]: Reverses a list of patches (swapping additions and deletions).
169//!
170//! ### Granular Application
171//!
172//! - [`apply_hunk_to_lines()`]: Applies a single hunk to a mutable vector of lines in-place.
173//! - [`find_hunk_location()`]: Finds the location to apply a hunk to a given text content without modifying it.
174//! - [`find_hunk_location_in_lines()`]: Finds the location to apply a hunk to a slice of lines without modifying it.
175//!
176//! ### Core Data Structures
177//!
178//! - [`Patch`]: Represents all the changes for a single file. It contains the
179//! target file path and a list of hunks.
180//! - [`Hunk`]: Represents a single block of changes within a patch, corresponding
181//! to a block of changes (like a `@@ ... @@` section in a unified diff).
182//! For **Conflict Markers**, the "before" block is treated as deletions and the
183//! "after" block as additions.
184//!
185//! ### Context-Driven Matching
186//!
187//! The core philosophy of `mpatch` is to ignore strict line numbers. Instead, it
188//! searches for the *context* of a hunkâthe lines that are unchanged or being
189//! deleted.
190//!
191//! - **Primary Search:** It first looks for an exact, character-for-character match
192//! of the hunk's context.
193//! - **Ambiguity Resolution:** If the same context appears in multiple places,
194//! `mpatch` uses the line numbers (e.g., from the `@@ ... @@` header) as a *hint* to
195//! find the most likely location.
196//! Note that patches derived from **Conflict Markers** typically lack line numbers,
197//! so ambiguity cannot be resolved this way.
198//! - **Fuzzy Matching:** If no exact match is found, it uses a similarity algorithm
199//! to find the *best* fuzzy match, making it resilient to minor changes in the
200//! surrounding code. It is also robust against indentation differences (e.g.,
201//! patches nested in Markdown lists).
202//! - **Smart Indentation:** When applying a patch via fuzzy matching, `mpatch`
203//! dynamically adjusts the indentation of added lines to match the surrounding
204//! code in the target file, preventing style corruption.
205//!
206//! ## Advanced Usage
207//!
208//! ### Configuring [`ApplyOptions`]
209//!
210//! The behavior of the `apply` functions is controlled by the [`ApplyOptions`] struct.
211//! `mpatch` provides several convenient ways to construct it:
212//!
213//! ````rust
214//! use mpatch::ApplyOptions;
215//!
216//! // For default behavior (fuzzy matching enabled, not a dry run)
217//! let default_options = ApplyOptions::new();
218//!
219//! // For common presets
220//! let dry_run_options = ApplyOptions::dry_run();
221//! let exact_options = ApplyOptions::exact();
222//!
223//! // For custom configurations using the new fluent methods
224//! let custom_fluent = ApplyOptions::new()
225//! .with_dry_run(true)
226//! .with_fuzz_factor(0.9);
227//!
228//! // For complex configurations using the builder pattern
229//! let custom_builder = ApplyOptions::builder()
230//! .dry_run(true)
231//! .fuzz_factor(0.9)
232//! .build();
233//!
234//! assert_eq!(custom_fluent.dry_run, custom_builder.dry_run);
235//! assert_eq!(custom_fluent.fuzz_factor, custom_builder.fuzz_factor);
236//! ````
237//!
238//! ### In-Memory Operations and Error Handling
239//!
240//! This example demonstrates how to use [`apply_patch_to_content()`] for in-memory
241//! operations and how to programmatically handle cases where a patch only
242//! partially applies.
243//!
244//! ````rust
245//! use mpatch::{parse_single_patch, apply_patch_to_content, HunkApplyError};
246//!
247//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
248//! // 1. Define original content and a patch where the second hunk will fail.
249//! let original_content = "line 1\nline 2\nline 3\n\nline 5\nline 6\nline 7\n";
250//! let diff_content = r#"
251//! ```diff
252//! --- a/partial.txt
253//! +++ b/partial.txt
254//! @@ -1,3 +1,3 @@
255//! line 1
256//! -line 2
257//! +line two
258//! line 3
259//! @@ -5,3 +5,3 @@
260//! line 5
261//! -line WRONG CONTEXT
262//! +line six
263//! line 7
264//! ```
265//! "#;
266//!
267//! // 2. Parse the diff.
268//! let patch = parse_single_patch(diff_content)?;
269//!
270//! // 3. Apply the patch to the content in memory.
271//! let options = mpatch::ApplyOptions::exact();
272//! let result = apply_patch_to_content(&patch, Some(original_content), &options);
273//!
274//! // 4. Verify that the patch did not apply cleanly.
275//! assert!(!result.report.all_applied_cleanly());
276//!
277//! // 5. Inspect the specific failures.
278//! let failures = result.report.failures();
279//! assert_eq!(failures.len(), 1);
280//! assert_eq!(failures[0].hunk_index, 2); // Hunk indices are 1-based.
281//! assert!(matches!(failures[0].reason, HunkApplyError::ContextNotFound));
282//!
283//! // 6. Verify that the content was still partially modified by the successful first hunk.
284//! let expected_content = "line 1\nline two\nline 3\n\nline 5\nline 6\nline 7\n";
285//! assert_eq!(result.new_content, expected_content);
286//! # Ok(())
287//! # }
288//! ````
289//!
290//! ### Strict Apply-or-Fail Workflow with `try_` functions
291//!
292//! The previous example showed how to manually check `result.report.all_applied_cleanly()`
293//! to detect partial failures. For workflows where any failed hunk should be treated as a
294//! hard error, `mpatch` provides "strict" variants of the apply functions.
295//!
296//! - [`try_apply_patch_to_file()`]
297//! - [`try_apply_patch_to_content()`]
298//! - [`try_apply_patch_to_lines()`]
299//!
300//! These functions return a `Result` where a partial application is mapped to a
301//! `Err(StrictApplyError::PartialApply { .. })`. This simplifies the common
302//! apply-or-fail pattern.
303//!
304//! ````rust
305//! use mpatch::{parse_single_patch, try_apply_patch_to_content, ApplyOptions, StrictApplyError};
306//!
307//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
308//! let original_content = "line 1\nline 2\n";
309//! let failing_diff = r#"
310//! ```diff
311//! --- a/file.txt
312//! +++ b/file.txt
313//! @@ -1,2 +1,2 @@
314//! line 1
315//! -WRONG CONTEXT
316//! +line two
317//! ```
318//! "#;
319//! let patch = parse_single_patch(failing_diff)?;
320//! let options = ApplyOptions::exact();
321//!
322//! // Using the try_ variant simplifies error handling.
323//! let result = try_apply_patch_to_content(&patch, Some(original_content), &options);
324//!
325//! assert!(matches!(result, Err(StrictApplyError::PartialApply { .. })));
326//! # Ok(())
327//! # }
328//! ````
329//!
330//! ### Step-by-Step Application with `HunkApplier`
331//!
332//! For maximum control, you can use the [`HunkApplier`] iterator to apply hunks
333//! one at a time and inspect the state between each step.
334//!
335//! ````rust
336//! use mpatch::{parse_single_patch, HunkApplier, HunkApplyStatus, ApplyOptions};
337//!
338//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
339//! // 1. Define original content and a patch.
340//! let original_lines = vec!["line 1", "line 2", "line 3"];
341//! let diff_content = r#"
342//! ```diff
343//! --- a/file.txt
344//! +++ b/file.txt
345//! @@ -2,1 +2,1 @@
346//! -line 2
347//! +line two
348//! ```
349//! "#;
350//! let patch = parse_single_patch(diff_content)?;
351//! let options = ApplyOptions::new();
352//!
353//! // 2. Create the applier.
354//! let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
355//!
356//! // 3. Apply the first (and only) hunk.
357//! let status = applier.next().unwrap();
358//! assert!(matches!(status, HunkApplyStatus::Applied { .. }));
359//!
360//! // 4. Check that there are no more hunks.
361//! assert!(applier.next().is_none());
362//!
363//! // 5. Finalize the content.
364//! let new_content = applier.into_content();
365//! assert_eq!(new_content, "line 1\nline two\nline 3\n");
366//! # Ok(())
367//! # }
368//! ````
369//!
370//! ### Creating Patches
371//!
372//! You can also use `mpatch` to generate patches by comparing two strings using
373//! [`Patch::from_texts()`].
374//!
375//! ````rust
376//! use mpatch::Patch;
377//!
378//! let old_text = "fn main() { println!(\"Old\"); }";
379//! let new_text = "fn main() { println!(\"New\"); }";
380//!
381//! // Create a patch with 3 lines of context
382//! let patch = Patch::from_texts("src/main.rs", old_text, new_text, 3).unwrap();
383//!
384//! assert_eq!(patch.hunks.len(), 1);
385//! ````
386//!
387//! ## Feature Flags
388//!
389//! `mpatch` includes the following optional features:
390//!
391//! ### `parallel`
392//!
393//! - **Enabled by default.**
394//! - This feature enables parallel processing for the fuzzy matching algorithm using the
395//! [`rayon`](https://crates.io/crates/rayon) crate. When an exact match for a hunk
396//! is not found, `mpatch` performs a computationally intensive search for the best
397//! fuzzy match. The `parallel` feature significantly speeds up this process on
398//! multi-core systems by distributing the search across multiple threads.
399//!
400//! - **To disable this feature**, specify `default-features = false` in your `Cargo.toml`:
401//! ```toml
402//! [dependencies]
403//! mpatch = { version = "1.6.4", default-features = false }
404//! ```
405//! You might want to disable this feature if you are compiling for a target that
406//! does not support threading (like `wasm32-unknown-unknown`) or if you want to
407//! minimize dependencies and binary size.
408use log::{debug, info, trace, warn};
409#[cfg(feature = "parallel")]
410use rayon::prelude::*;
411use similar::udiff::unified_diff;
412use similar::TextDiff;
413use std::fs;
414use std::path::{Path, PathBuf};
415use thiserror::Error;
416
417// --- Error Types ---
418
419/// Represents errors that can occur during the parsing of a diff file.
420///
421/// This error is returned by parsing functions like [`parse_patches()`] and
422/// [`parse_auto()`] when the input content is syntactically invalid.
423///
424/// Note that [`parse_diffs()`] is lenient and will typically skip blocks that do
425/// not look like valid patches rather than returning this error.
426///
427/// # Examples
428///
429/// ````rust
430/// use mpatch::{parse_patches, ParseError};
431///
432/// // This raw diff is missing the required `--- a/path` header.
433/// let malformed_diff = r#"
434/// @@ -1,2 +1,2 @@
435/// -foo
436/// +bar
437/// "#;
438///
439/// let result = parse_patches(malformed_diff);
440///
441/// assert!(matches!(result, Err(ParseError::MissingFileHeader { .. })));
442/// ````
443#[derive(Error, Debug, PartialEq)]
444pub enum ParseError {
445 /// A diff block or raw patch was found, but it was missing the `--- a/path/to/file`
446 /// header required to identify the target file.
447 ///
448 /// # Examples
449 ///
450 /// ```
451 /// use mpatch::ParseError;
452 /// let err = ParseError::MissingFileHeader { line: 10 };
453 /// ```
454 #[error("Diff block starting on line {line} was found without a file path header (e.g., '--- a/path/to/file')")]
455 MissingFileHeader {
456 /// The line number where the diff block started.
457 ///
458 /// # Examples
459 ///
460 /// ```
461 /// use mpatch::ParseError;
462 /// let err = ParseError::MissingFileHeader { line: 10 };
463 /// match err {
464 /// ParseError::MissingFileHeader { line } => assert_eq!(line, 10),
465 /// }
466 /// ```
467 line: usize,
468 },
469}
470
471/// Represents errors that can occur when parsing a diff expected to contain exactly one patch.
472///
473/// This enum is returned by [`parse_single_patch()`] when the input content does not
474/// result in exactly one `Patch` object. It handles errors from format detection,
475/// parsing, and patch count validation.
476///
477/// # Examples
478///
479/// ````rust
480/// use mpatch::{parse_single_patch, SingleParseError};
481///
482/// // This diff content contains two patches, which is not allowed.
483/// let multi_patch_diff = r#"
484/// ```diff
485/// --- a/file1.txt
486/// +++ b/file1.txt
487/// @@ -1 +1 @@
488/// -a
489/// +b
490/// --- a/file2.txt
491/// +++ b/file2.txt
492/// @@ -1 +1 @@
493/// -c
494/// +d
495/// ```
496/// "#;
497///
498/// let result = parse_single_patch(multi_patch_diff);
499/// assert!(matches!(result, Err(SingleParseError::MultiplePatchesFound(2))));
500/// ````
501#[derive(Error, Debug, PartialEq)]
502#[non_exhaustive]
503pub enum SingleParseError {
504 /// An error occurred during the underlying diff parsing.
505 ///
506 /// # Examples
507 ///
508 /// ```
509 /// use mpatch::{SingleParseError, ParseError};
510 /// let err = SingleParseError::Parse(ParseError::MissingFileHeader { line: 1 });
511 /// ```
512 #[error("Failed to parse diff content")]
513 Parse(#[from] ParseError),
514
515 /// The provided diff content did not contain any valid patches (Markdown blocks,
516 /// Unified Diffs, or Conflict Markers).
517 ///
518 /// # Examples
519 ///
520 /// ```
521 /// use mpatch::SingleParseError;
522 /// let err = SingleParseError::NoPatchesFound;
523 /// ```
524 #[error("No patches were found in the provided diff content")]
525 NoPatchesFound,
526
527 /// The provided diff content contained patches for more than one file, which is not
528 /// supported by this function. Use [`parse_diffs()`] for multi-file operations.
529 ///
530 /// # Examples
531 ///
532 /// ```
533 /// use mpatch::SingleParseError;
534 /// let err = SingleParseError::MultiplePatchesFound(3);
535 /// ```
536 #[error(
537 "Found patches for multiple files ({0} patches), but this function only supports single-file diffs"
538 )]
539 MultiplePatchesFound(usize),
540}
541
542/// Represents "hard" errors that can occur during patch operations.
543///
544/// This error type is returned by functions like [`apply_patch_to_file()`] for
545/// unrecoverable issues such as I/O errors, permission problems, or security
546/// violations like path traversal. It is distinct from a partial apply, which
547/// is handled by the result structs.
548///
549/// # Examples
550///
551/// ````rust
552/// # use mpatch::{parse_single_patch, apply_patch_to_file, ApplyOptions, PatchError};
553/// # use tempfile::tempdir;
554/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
555/// let dir = tempdir()?;
556/// // Note: "missing.txt" does not exist in the directory.
557///
558/// let diff = r#"
559/// ```diff
560/// --- a/missing.txt
561/// +++ b/missing.txt
562/// @@ -1 +1 @@
563/// -foo
564/// +bar
565/// ```
566/// "#;
567/// let patch = parse_single_patch(diff)?;
568/// let options = ApplyOptions::new();
569///
570/// // This will fail because the target file doesn't exist and it's not a creation patch.
571/// let result = apply_patch_to_file(&patch, dir.path(), options);
572///
573/// assert!(matches!(result, Err(PatchError::TargetNotFound(_))));
574/// # Ok(())
575/// # }
576/// ````
577#[derive(Error, Debug)]
578pub enum PatchError {
579 /// The patch attempted to access a path outside the target directory.
580 /// This is a security measure to prevent malicious patches from modifying
581 /// unintended files (e.g., `--- a/../../etc/passwd`).
582 ///
583 /// # Examples
584 ///
585 /// ```
586 /// use mpatch::PatchError;
587 /// use std::path::PathBuf;
588 /// let err = PatchError::PathTraversal(PathBuf::from("../../etc/passwd"));
589 /// ```
590 #[error("Path '{0}' resolves outside the target directory. Aborting for security.")]
591 PathTraversal(PathBuf),
592 /// The target file for a patch could not be found, and the patch did not
593 /// appear to be for file creation (i.e., its first hunk was not an addition-only hunk).
594 ///
595 /// # Examples
596 ///
597 /// ```
598 /// use mpatch::PatchError;
599 /// use std::path::PathBuf;
600 /// let err = PatchError::TargetNotFound(PathBuf::from("missing.txt"));
601 /// ```
602 #[error("Target file not found for patching: {0}")]
603 TargetNotFound(PathBuf),
604 /// The user does not have permission to read or write to the specified path.
605 ///
606 /// # Examples
607 ///
608 /// ```
609 /// use mpatch::PatchError;
610 /// use std::path::PathBuf;
611 /// let err = PatchError::PermissionDenied { path: PathBuf::from("readonly.txt") };
612 /// ```
613 #[error("Permission denied for path: {path:?}")]
614 PermissionDenied {
615 /// The path that could not be accessed.
616 ///
617 /// # Examples
618 ///
619 /// ```
620 /// use mpatch::PatchError;
621 /// use std::path::PathBuf;
622 /// let err = PatchError::PermissionDenied { path: PathBuf::from("readonly.txt") };
623 /// match err {
624 /// PatchError::PermissionDenied { path } => assert_eq!(path.to_str(), Some("readonly.txt")),
625 /// _ => unreachable!(),
626 /// }
627 /// ```
628 path: PathBuf,
629 },
630 /// The target path for a patch exists but is a directory, not a file.
631 ///
632 /// # Examples
633 ///
634 /// ```
635 /// use mpatch::PatchError;
636 /// use std::path::PathBuf;
637 /// let err = PatchError::TargetIsDirectory { path: PathBuf::from("src/") };
638 /// ```
639 #[error("Target path is a directory, not a file: {path:?}")]
640 TargetIsDirectory {
641 /// The path that resolved to a directory.
642 ///
643 /// # Examples
644 ///
645 /// ```
646 /// use mpatch::PatchError;
647 /// use std::path::PathBuf;
648 /// let err = PatchError::TargetIsDirectory { path: PathBuf::from("src/") };
649 /// match err {
650 /// PatchError::TargetIsDirectory { path } => assert_eq!(path.to_str(), Some("src/")),
651 /// _ => unreachable!(),
652 /// }
653 /// ```
654 path: PathBuf,
655 },
656 /// An I/O error occurred while reading or writing a file.
657 /// This is a "hard" error that stops the entire process.
658 ///
659 /// # Examples
660 ///
661 /// ```
662 /// use mpatch::PatchError;
663 /// use std::path::PathBuf;
664 /// use std::io::{Error, ErrorKind};
665 /// let err = PatchError::Io { path: PathBuf::from("file.txt"), source: Error::new(ErrorKind::Other, "oh no") };
666 /// ```
667 #[error("I/O error while processing {path:?}: {source}")]
668 Io {
669 /// The path associated with the I/O error.
670 ///
671 /// # Examples
672 ///
673 /// ```
674 /// use mpatch::PatchError;
675 /// use std::path::PathBuf;
676 /// use std::io::{Error, ErrorKind};
677 /// let err = PatchError::Io { path: PathBuf::from("file.txt"), source: Error::new(ErrorKind::Other, "oh no") };
678 /// match err {
679 /// PatchError::Io { path, .. } => assert_eq!(path.to_str(), Some("file.txt")),
680 /// _ => unreachable!(),
681 /// }
682 /// ```
683 path: PathBuf,
684 /// The underlying I/O error.
685 ///
686 /// # Examples
687 ///
688 /// ```
689 /// use mpatch::PatchError;
690 /// use std::path::PathBuf;
691 /// use std::io::{Error, ErrorKind};
692 /// let err = PatchError::Io { path: PathBuf::from("file.txt"), source: Error::new(ErrorKind::Other, "oh no") };
693 /// match err {
694 /// PatchError::Io { source, .. } => assert_eq!(source.kind(), ErrorKind::Other),
695 /// _ => unreachable!(),
696 /// }
697 /// ```
698 #[source]
699 source: std::io::Error,
700 },
701}
702
703/// Represents errors that can occur during "strict" apply operations.
704///
705/// This enum is returned by functions like [`try_apply_patch_to_file()`] and
706/// [`try_apply_patch_to_content()`], which treat partial applications as an error.
707/// It consolidates hard failures ([`PatchError`]) and soft failures ([`StrictApplyError::PartialApply`])
708/// into a single error type for easier handling in apply-or-fail workflows.
709///
710/// # Examples
711///
712/// ````rust
713/// use mpatch::{parse_single_patch, try_apply_patch_to_content, ApplyOptions, StrictApplyError};
714///
715/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
716/// let original_content = "line 1\nline 2\n";
717/// // This patch will fail because the context "WRONG CONTEXT" is not in the original content.
718/// let failing_diff = r#"
719/// ```diff
720/// --- a/file.txt
721/// +++ b/file.txt
722/// @@ -1,2 +1,2 @@
723/// line 1
724/// -WRONG CONTEXT
725/// +line two
726/// ```
727/// "#;
728/// let patch = parse_single_patch(failing_diff)?;
729/// let options = ApplyOptions::exact();
730///
731/// // Using the try_ variant simplifies error handling for partial applications.
732/// let result = try_apply_patch_to_content(&patch, Some(original_content), &options);
733///
734/// assert!(matches!(result, Err(StrictApplyError::PartialApply { .. })));
735/// if let Err(StrictApplyError::PartialApply { report }) = result {
736/// assert!(!report.all_applied_cleanly());
737/// }
738/// # Ok(())
739/// # }
740/// ````
741#[derive(Error, Debug)]
742#[non_exhaustive]
743pub enum StrictApplyError {
744 /// A hard error occurred during the patch operation (e.g., I/O error,
745 /// file not found).
746 ///
747 /// # Examples
748 ///
749 /// ```
750 /// use mpatch::{StrictApplyError, PatchError};
751 /// use std::path::PathBuf;
752 /// let err = StrictApplyError::Patch(PatchError::TargetNotFound(PathBuf::from("file.txt")));
753 /// ```
754 #[error(transparent)]
755 Patch(#[from] PatchError),
756
757 /// The patch was only partially applied, with some hunks failing.
758 ///
759 /// # Examples
760 ///
761 /// ```
762 /// use mpatch::{StrictApplyError, ApplyResult};
763 /// let report = ApplyResult { hunk_results: vec![] };
764 /// let err = StrictApplyError::PartialApply { report };
765 /// ```
766 #[error("Patch applied partially. See report for details.")]
767 PartialApply {
768 /// The detailed report of the operation, including which hunks succeeded/failed.
769 ///
770 /// # Examples
771 ///
772 /// ```
773 /// use mpatch::{StrictApplyError, ApplyResult};
774 /// let report = ApplyResult { hunk_results: vec![] };
775 /// let err = StrictApplyError::PartialApply { report };
776 /// match err {
777 /// StrictApplyError::PartialApply { report } => assert!(report.all_applied_cleanly()),
778 /// _ => unreachable!(),
779 /// }
780 /// ```
781 report: ApplyResult,
782 },
783}
784
785/// Represents errors that can occur during the high-level [`patch_content_str()`] operation.
786///
787/// This enum consolidates all possible failures from the one-shot workflow,
788/// including parsing errors, finding the wrong number of patches, or failures
789/// during the strict application process.
790///
791/// # Examples
792///
793/// ````rust
794/// use mpatch::{patch_content_str, ApplyOptions, OneShotError};
795///
796/// // This diff content contains two patches, which is not allowed by `patch_content_str`.
797/// let multi_patch_diff = r#"
798/// ```diff
799/// --- a/file1.txt
800/// +++ b/file1.txt
801/// @@ -1 +1 @@
802/// -a
803/// +b
804/// --- a/file2.txt
805/// +++ b/file2.txt
806/// @@ -1 +1 @@
807/// -c
808/// +d
809/// ```
810/// "#;
811///
812/// let options = ApplyOptions::new();
813/// let result = patch_content_str(multi_patch_diff, Some("a\n"), &options);
814///
815/// assert!(matches!(result, Err(OneShotError::MultiplePatchesFound(2))));
816/// ````
817#[derive(Error, Debug)]
818#[non_exhaustive]
819pub enum OneShotError {
820 /// An error occurred while parsing the diff content.
821 ///
822 /// # Examples
823 ///
824 /// ```
825 /// use mpatch::{OneShotError, ParseError};
826 /// let err = OneShotError::Parse(ParseError::MissingFileHeader { line: 1 });
827 /// ```
828 #[error("Failed to parse diff content")]
829 Parse(#[from] ParseError),
830
831 /// An error occurred while applying the patch. This includes partial applications.
832 ///
833 /// # Examples
834 ///
835 /// ```
836 /// use mpatch::{OneShotError, StrictApplyError, PatchError};
837 /// use std::path::PathBuf;
838 /// let err = OneShotError::Apply(StrictApplyError::Patch(PatchError::TargetNotFound(PathBuf::from("file.txt"))));
839 /// ```
840 #[error("Failed to apply patch")]
841 Apply(#[from] StrictApplyError),
842
843 /// The provided diff content did not contain any valid patches (Markdown blocks,
844 /// Unified Diffs, or Conflict Markers).
845 ///
846 /// # Examples
847 ///
848 /// ```
849 /// use mpatch::OneShotError;
850 /// let err = OneShotError::NoPatchesFound;
851 /// ```
852 #[error("No patches were found in the provided diff content")]
853 NoPatchesFound,
854
855 /// The provided diff content contained patches for more than one file, which is not
856 /// supported by this simplified function. Use [`parse_diffs()`] and
857 /// [`apply_patches_to_dir()`] for multi-file operations.
858 ///
859 /// # Examples
860 ///
861 /// ```
862 /// use mpatch::OneShotError;
863 /// let err = OneShotError::MultiplePatchesFound(2);
864 /// ```
865 #[error(
866 "Found patches for multiple files ({0} files), but this function only supports single-file diffs"
867 )]
868 MultiplePatchesFound(usize),
869}
870
871/// The reason a hunk failed to apply.
872///
873/// This enum provides specific details about why a hunk could not be applied to the
874/// target content. It is found within the [`HunkApplyStatus::Failed`] variant.
875///
876/// # Examples
877///
878/// ````rust
879/// use mpatch::{apply_patch_to_content, parse_single_patch, ApplyOptions, HunkApplyStatus, HunkApplyError};
880///
881/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
882/// let original_content = "line 1\nline 2\n";
883/// // This patch will fail because the context is wrong.
884/// let diff = r#"
885/// ```diff
886/// --- a/file.txt
887/// +++ b/file.txt
888/// @@ -1,2 +1,2 @@
889/// line 1
890/// -WRONG CONTEXT
891/// +line two
892/// ```
893/// "#;
894/// let patch = parse_single_patch(diff)?;
895/// let options = ApplyOptions::exact();
896///
897/// let result = apply_patch_to_content(&patch, Some(original_content), &options);
898///
899/// // We can inspect the status of the first hunk.
900/// let hunk_status = &result.report.hunk_results[0];
901///
902/// assert!(matches!(hunk_status, HunkApplyStatus::Failed(HunkApplyError::ContextNotFound)));
903/// # Ok(())
904/// # }
905/// ````
906#[derive(Error, Debug, Clone, PartialEq)]
907pub enum HunkApplyError {
908 /// The context lines for the hunk could not be found in the target file.
909 ///
910 /// # Examples
911 ///
912 /// ```
913 /// use mpatch::HunkApplyError;
914 /// let err = HunkApplyError::ContextNotFound;
915 /// ```
916 #[error("Context not found")]
917 ContextNotFound,
918 /// An exact match for the hunk's context was found in multiple locations,
919 /// and the ambiguity could not be resolved by the line number hint.
920 ///
921 /// # Examples
922 ///
923 /// ```
924 /// use mpatch::HunkApplyError;
925 /// let err = HunkApplyError::AmbiguousExactMatch(vec![10, 20]);
926 /// ```
927 #[error("Ambiguous exact match found at lines: {0:?}")]
928 AmbiguousExactMatch(Vec<usize>),
929 /// A fuzzy match for the hunk's context was found in multiple locations with
930 /// the same top score, and the ambiguity could not be resolved.
931 ///
932 /// # Examples
933 ///
934 /// ```
935 /// use mpatch::HunkApplyError;
936 /// let err = HunkApplyError::AmbiguousFuzzyMatch(vec![(5, 3), (15, 3)]);
937 /// ```
938 #[error("Ambiguous fuzzy match found at locations: {0:?}")]
939 AmbiguousFuzzyMatch(Vec<(usize, usize)>),
940 /// The best fuzzy match found was below the required similarity threshold.
941 ///
942 /// # Examples
943 ///
944 /// ```
945 /// use mpatch::{HunkApplyError, HunkLocation};
946 /// let err = HunkApplyError::FuzzyMatchBelowThreshold { best_score: 0.5, threshold: 0.7, location: HunkLocation { start_index: 0, length: 5 } };
947 /// ```
948 #[error("Best fuzzy match at {location} (score: {best_score:.3}) was below threshold ({threshold:.3})")]
949 FuzzyMatchBelowThreshold {
950 /// The similarity score of the best match found.
951 ///
952 /// # Examples
953 ///
954 /// ```
955 /// use mpatch::{HunkApplyError, HunkLocation};
956 /// let err = HunkApplyError::FuzzyMatchBelowThreshold { best_score: 0.5, threshold: 0.7, location: HunkLocation { start_index: 0, length: 5 } };
957 /// match err {
958 /// HunkApplyError::FuzzyMatchBelowThreshold { best_score, .. } => assert_eq!(best_score, 0.5),
959 /// _ => unreachable!(),
960 /// }
961 /// ```
962 best_score: f64,
963 /// The minimum similarity score required for a successful match.
964 ///
965 /// # Examples
966 ///
967 /// ```
968 /// use mpatch::{HunkApplyError, HunkLocation};
969 /// let err = HunkApplyError::FuzzyMatchBelowThreshold { best_score: 0.5, threshold: 0.7, location: HunkLocation { start_index: 0, length: 5 } };
970 /// match err {
971 /// HunkApplyError::FuzzyMatchBelowThreshold { threshold, .. } => assert_eq!(threshold, 0.7),
972 /// _ => unreachable!(),
973 /// }
974 /// ```
975 threshold: f32,
976 /// The location of the best-scoring (but rejected) fuzzy match.
977 ///
978 /// # Examples
979 ///
980 /// ```
981 /// use mpatch::{HunkApplyError, HunkLocation};
982 /// let err = HunkApplyError::FuzzyMatchBelowThreshold { best_score: 0.5, threshold: 0.7, location: HunkLocation { start_index: 0, length: 5 } };
983 /// match err {
984 /// HunkApplyError::FuzzyMatchBelowThreshold { location, .. } => assert_eq!(location.length, 5),
985 /// _ => unreachable!(),
986 /// }
987 /// ```
988 location: HunkLocation,
989 },
990}
991
992/// Describes the method used to successfully locate and apply a hunk.
993///
994/// This enum is included in the [`HunkApplyStatus::Applied`] variant and provides
995/// insight into how `mpatch` found the location for a hunk, which is useful for
996/// logging and diagnostics.
997///
998/// # Examples
999///
1000/// ````rust
1001/// use mpatch::{apply_patch_to_content, parse_single_patch, ApplyOptions, HunkApplyStatus, MatchType};
1002///
1003/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1004/// // The file content has an extra space, which will prevent an `Exact` match.
1005/// let original_content = "line 1 \nline 2\n";
1006/// let diff = r#"
1007/// ```diff
1008/// --- a/file.txt
1009/// +++ b/file.txt
1010/// @@ -1,2 +1,2 @@
1011/// line 1
1012/// -line 2
1013/// +line two
1014/// ```
1015/// "#;
1016/// let patch = parse_single_patch(diff)?;
1017/// let options = ApplyOptions::new();
1018///
1019/// let result = apply_patch_to_content(&patch, Some(original_content), &options);
1020/// let hunk_status = &result.report.hunk_results[0];
1021///
1022/// assert!(matches!(hunk_status, HunkApplyStatus::Applied { match_type: MatchType::ExactIgnoringWhitespace, .. }));
1023/// # Ok(())
1024/// # }
1025/// ````
1026#[derive(Debug, Clone, PartialEq)]
1027pub enum MatchType {
1028 /// An exact, character-for-character match of the context/deletion lines.
1029 ///
1030 /// # Examples
1031 ///
1032 /// ```
1033 /// use mpatch::MatchType;
1034 /// let match_type = MatchType::Exact;
1035 /// ```
1036 Exact,
1037 /// An exact match after ignoring trailing whitespace on each line.
1038 ///
1039 /// # Examples
1040 ///
1041 /// ```
1042 /// use mpatch::MatchType;
1043 /// let match_type = MatchType::ExactIgnoringWhitespace;
1044 /// ```
1045 ExactIgnoringWhitespace,
1046 /// A fuzzy match found using a similarity algorithm.
1047 ///
1048 /// # Examples
1049 ///
1050 /// ```
1051 /// use mpatch::MatchType;
1052 /// let match_type = MatchType::Fuzzy { score: 0.85 };
1053 /// ```
1054 Fuzzy {
1055 /// The similarity score of the match (0.0 to 1.0).
1056 ///
1057 /// # Examples
1058 ///
1059 /// ```
1060 /// use mpatch::MatchType;
1061 /// let match_type = MatchType::Fuzzy { score: 0.85 };
1062 /// match match_type {
1063 /// MatchType::Fuzzy { score } => assert_eq!(score, 0.85),
1064 /// _ => unreachable!(),
1065 /// }
1066 /// ```
1067 score: f64,
1068 },
1069}
1070
1071/// The result of applying a single hunk.
1072///
1073/// This enum is returned by [`apply_hunk_to_lines()`] and is the item type for the
1074/// [`HunkApplier`] iterator. It provides a detailed outcome for each individual
1075/// hunk within a patch.
1076///
1077/// # Examples
1078///
1079/// ````rust
1080/// use mpatch::{apply_hunk_to_lines, parse_single_patch, ApplyOptions, HunkApplyStatus, HunkApplyError};
1081///
1082/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1083/// let diff = r#"
1084/// ```diff
1085/// --- a/file.txt
1086/// +++ b/file.txt
1087/// @@ -1,2 +1,2 @@
1088/// line 1
1089/// -line 2
1090/// +line two
1091/// ```
1092/// "#;
1093/// let hunk = parse_single_patch(diff)?.hunks.remove(0);
1094/// let options = ApplyOptions::new();
1095///
1096/// // --- Success Case ---
1097/// let mut lines_success = vec!["line 1".to_string(), "line 2".to_string()];
1098/// let status_success = apply_hunk_to_lines(&hunk, &mut lines_success, &options);
1099/// assert!(matches!(status_success, HunkApplyStatus::Applied { .. }));
1100/// assert_eq!(lines_success, vec!["line 1", "line two"]);
1101///
1102/// // --- Failure Case ---
1103/// let mut lines_fail = vec!["wrong".to_string(), "content".to_string()];
1104/// let fail_status = apply_hunk_to_lines(&hunk, &mut lines_fail, &options);
1105/// assert!(matches!(fail_status, HunkApplyStatus::Failed(HunkApplyError::FuzzyMatchBelowThreshold { .. })));
1106/// assert_eq!(lines_fail, vec!["wrong", "content"]); // Content is unchanged
1107/// # Ok(())
1108/// # }
1109/// ````
1110#[derive(Debug, Clone, PartialEq)]
1111pub enum HunkApplyStatus {
1112 /// The hunk was applied successfully.
1113 ///
1114 /// # Examples
1115 ///
1116 /// ```
1117 /// use mpatch::{HunkApplyStatus, HunkLocation, MatchType};
1118 /// let status = HunkApplyStatus::Applied {
1119 /// location: HunkLocation { start_index: 0, length: 2 },
1120 /// match_type: MatchType::Exact,
1121 /// replaced_lines: vec!["old line".to_string()],
1122 /// };
1123 /// ```
1124 Applied {
1125 /// The location where the hunk was applied.
1126 ///
1127 /// # Examples
1128 ///
1129 /// ```
1130 /// use mpatch::{HunkApplyStatus, HunkLocation, MatchType};
1131 /// let status = HunkApplyStatus::Applied {
1132 /// location: HunkLocation { start_index: 0, length: 2 },
1133 /// match_type: MatchType::Exact,
1134 /// replaced_lines: vec!["old line".to_string()],
1135 /// };
1136 /// match status {
1137 /// HunkApplyStatus::Applied { location, .. } => assert_eq!(location.start_index, 0),
1138 /// _ => unreachable!(),
1139 /// }
1140 /// ```
1141 location: HunkLocation,
1142 /// The type of match that was used to find the location.
1143 ///
1144 /// # Examples
1145 ///
1146 /// ```
1147 /// use mpatch::{HunkApplyStatus, HunkLocation, MatchType};
1148 /// let status = HunkApplyStatus::Applied {
1149 /// location: HunkLocation { start_index: 0, length: 2 },
1150 /// match_type: MatchType::Exact,
1151 /// replaced_lines: vec!["old line".to_string()],
1152 /// };
1153 /// match status {
1154 /// HunkApplyStatus::Applied { match_type, .. } => assert!(matches!(match_type, MatchType::Exact)),
1155 /// _ => unreachable!(),
1156 /// }
1157 /// ```
1158 match_type: MatchType,
1159 /// The original lines that were replaced by the hunk.
1160 ///
1161 /// # Examples
1162 ///
1163 /// ```
1164 /// use mpatch::{HunkApplyStatus, HunkLocation, MatchType};
1165 /// let status = HunkApplyStatus::Applied {
1166 /// location: HunkLocation { start_index: 0, length: 2 },
1167 /// match_type: MatchType::Exact,
1168 /// replaced_lines: vec!["old line".to_string()],
1169 /// };
1170 /// match status {
1171 /// HunkApplyStatus::Applied { replaced_lines, .. } => assert_eq!(replaced_lines.len(), 1),
1172 /// _ => unreachable!(),
1173 /// }
1174 /// ```
1175 replaced_lines: Vec<String>,
1176 },
1177 /// The hunk was skipped because it contained no effective changes.
1178 ///
1179 /// # Examples
1180 ///
1181 /// ```
1182 /// use mpatch::HunkApplyStatus;
1183 /// let status = HunkApplyStatus::SkippedNoChanges;
1184 /// ```
1185 SkippedNoChanges,
1186 /// The hunk failed to apply for the specified reason.
1187 ///
1188 /// # Examples
1189 ///
1190 /// ```
1191 /// use mpatch::{HunkApplyStatus, HunkApplyError};
1192 /// let status = HunkApplyStatus::Failed(HunkApplyError::ContextNotFound);
1193 /// ```
1194 Failed(HunkApplyError),
1195}
1196
1197/// Options for configuring how a patch is applied.
1198///
1199/// This struct controls the behavior of patch application functions like
1200/// [`apply_patch_to_file()`] and [`apply_patch_to_content()`]. It allows you to
1201/// enable dry-run mode, configure the fuzzy matching threshold, and more.
1202///
1203/// While you can construct it directly, it's often more convenient to use one of
1204/// the associated functions like [`ApplyOptions::new()`], [`ApplyOptions::dry_run()`],
1205/// or the fluent [`with_dry_run()`](ApplyOptions::with_dry_run) and
1206/// [`with_fuzz_factor()`](ApplyOptions::with_fuzz_factor) methods.
1207///
1208/// # Examples
1209///
1210/// ```
1211/// use mpatch::ApplyOptions;
1212///
1213/// // Direct construction for full control.
1214/// let custom_options = ApplyOptions {
1215/// dry_run: true,
1216/// fuzz_factor: 0.9,
1217/// };
1218///
1219/// // Using a convenience constructor for common cases.
1220/// let dry_run_options = ApplyOptions::dry_run();
1221/// assert_eq!(dry_run_options.dry_run, true);
1222///
1223/// // Using fluent methods for a chainable style.
1224/// let fluent_options = ApplyOptions::new()
1225/// .with_dry_run(true)
1226/// .with_fuzz_factor(0.5);
1227/// assert_eq!(fluent_options.fuzz_factor, 0.5);
1228/// ```
1229#[derive(Debug, Clone, Copy, PartialEq)]
1230pub struct ApplyOptions {
1231 /// If `true`, no files will be modified. Instead, a diff of the proposed
1232 /// changes will be generated and returned in [`PatchResult`].
1233 ///
1234 /// This is the primary way to preview the outcome of a patch operation without
1235 /// making any changes to the filesystem. When `dry_run` is enabled, functions
1236 /// like [`apply_patch_to_file()`] will populate the `diff` field of the
1237 /// returned [`PatchResult`].
1238 ///
1239 /// # Examples
1240 ///
1241 /// ```
1242 /// # use mpatch::ApplyOptions;
1243 /// // Create options for a dry run.
1244 /// let options = ApplyOptions {
1245 /// dry_run: true,
1246 /// fuzz_factor: 0.7,
1247 /// };
1248 ///
1249 /// assert!(options.dry_run);
1250 /// ```
1251 pub dry_run: bool,
1252 /// The similarity threshold for fuzzy matching (0.0 to 1.0).
1253 /// Higher is stricter. `0.0` disables fuzzy matching.
1254 ///
1255 /// # Examples
1256 ///
1257 /// ```
1258 /// # use mpatch::ApplyOptions;
1259 /// let options = ApplyOptions {
1260 /// dry_run: false,
1261 /// fuzz_factor: 0.85,
1262 /// };
1263 /// assert_eq!(options.fuzz_factor, 0.85);
1264 /// ```
1265 pub fuzz_factor: f32,
1266}
1267
1268impl Default for ApplyOptions {
1269 /// Creates a new [`ApplyOptions`] instance with default values.
1270 ///
1271 /// This is the standard way to get a default configuration, which has `dry_run`
1272 /// set to `false` and `fuzz_factor` set to `0.7`.
1273 ///
1274 /// # Returns
1275 ///
1276 /// A new [`ApplyOptions`] instance with default values.
1277 ///
1278 /// # Examples
1279 ///
1280 /// ```
1281 /// # use mpatch::ApplyOptions;
1282 /// let options: ApplyOptions = Default::default();
1283 ///
1284 /// assert_eq!(options.dry_run, false);
1285 /// assert_eq!(options.fuzz_factor, 0.7);
1286 /// ```
1287 fn default() -> Self {
1288 Self {
1289 dry_run: false,
1290 fuzz_factor: 0.7,
1291 }
1292 }
1293}
1294
1295impl ApplyOptions {
1296 /// Creates a new [`ApplyOptions`] instance with default values.
1297 ///
1298 /// This is an alias for [`ApplyOptions::default()`].
1299 ///
1300 /// # Returns
1301 ///
1302 /// A new [`ApplyOptions`] instance.
1303 ///
1304 /// # Examples
1305 ///
1306 /// ```
1307 /// # use mpatch::ApplyOptions;
1308 /// let options = ApplyOptions::new();
1309 /// assert_eq!(options.dry_run, false);
1310 /// assert_eq!(options.fuzz_factor, 0.7);
1311 /// ```
1312 pub fn new() -> Self {
1313 Self::default()
1314 }
1315
1316 /// Creates a new [`ApplyOptions`] instance configured for a dry run.
1317 ///
1318 /// All other options are set to their default values.
1319 ///
1320 /// # Returns
1321 ///
1322 /// A new [`ApplyOptions`] instance.
1323 ///
1324 /// # Examples
1325 ///
1326 /// ```
1327 /// # use mpatch::ApplyOptions;
1328 /// let options = ApplyOptions::dry_run();
1329 /// assert_eq!(options.dry_run, true);
1330 /// assert_eq!(options.fuzz_factor, 0.7);
1331 /// ```
1332 pub fn dry_run() -> Self {
1333 Self {
1334 dry_run: true,
1335 ..Self::default()
1336 }
1337 }
1338
1339 /// Creates a new [`ApplyOptions`] instance configured for an exact match (fuzz factor 0.0).
1340 ///
1341 /// All other options are set to their default values.
1342 ///
1343 /// # Returns
1344 ///
1345 /// A new [`ApplyOptions`] instance.
1346 ///
1347 /// # Examples
1348 ///
1349 /// ```
1350 /// # use mpatch::ApplyOptions;
1351 /// let options = ApplyOptions::exact();
1352 ///
1353 /// assert_eq!(options.dry_run, false);
1354 /// assert_eq!(options.fuzz_factor, 0.0);
1355 /// ```
1356 pub fn exact() -> Self {
1357 Self {
1358 fuzz_factor: 0.0,
1359 ..Self::default()
1360 }
1361 }
1362
1363 /// Returns a new [`ApplyOptions`] instance with the `dry_run` flag set.
1364 ///
1365 /// This is a fluent method that allows for chaining.
1366 ///
1367 /// # Arguments
1368 ///
1369 /// * `dry_run` - The boolean value to set.
1370 ///
1371 /// # Returns
1372 ///
1373 /// A new [`ApplyOptions`] instance with the updated setting.
1374 ///
1375 /// # Examples
1376 ///
1377 /// ```
1378 /// # use mpatch::ApplyOptions;
1379 /// let options = ApplyOptions::new().with_dry_run(true);
1380 /// assert_eq!(options.dry_run, true);
1381 ///
1382 /// let options2 = options.with_dry_run(false);
1383 /// assert_eq!(options2.dry_run, false);
1384 /// ```
1385 pub fn with_dry_run(mut self, dry_run: bool) -> Self {
1386 self.dry_run = dry_run;
1387 self
1388 }
1389
1390 /// Returns a new [`ApplyOptions`] instance with the `fuzz_factor` set.
1391 ///
1392 /// This is a fluent method that allows for chaining.
1393 ///
1394 /// # Arguments
1395 ///
1396 /// * `fuzz_factor` - The float value to set.
1397 ///
1398 /// # Returns
1399 ///
1400 /// A new [`ApplyOptions`] instance with the updated setting.
1401 ///
1402 /// # Examples
1403 ///
1404 /// ```
1405 /// # use mpatch::ApplyOptions;
1406 /// let options = ApplyOptions::new().with_fuzz_factor(0.9);
1407 /// assert_eq!(options.fuzz_factor, 0.9);
1408 ///
1409 /// let options2 = options.with_fuzz_factor(0.5);
1410 /// assert_eq!(options2.fuzz_factor, 0.5);
1411 /// ```
1412 pub fn with_fuzz_factor(mut self, fuzz_factor: f32) -> Self {
1413 self.fuzz_factor = fuzz_factor;
1414 self
1415 }
1416
1417 /// Creates a new builder for [`ApplyOptions`].
1418 ///
1419 /// This provides a classic builder pattern for constructing an [`ApplyOptions`] struct,
1420 /// which can be useful when the configuration is built conditionally or comes from
1421 /// multiple sources.
1422 ///
1423 /// # Returns
1424 ///
1425 /// A new [`ApplyOptionsBuilder`] instance.
1426 ///
1427 /// # Examples
1428 ///
1429 /// ```
1430 /// # use mpatch::ApplyOptions;
1431 /// let options = ApplyOptions::builder()
1432 /// .dry_run(true)
1433 /// .fuzz_factor(0.8)
1434 /// .build();
1435 ///
1436 /// assert_eq!(options.dry_run, true);
1437 /// assert_eq!(options.fuzz_factor, 0.8);
1438 /// ```
1439 pub fn builder() -> ApplyOptionsBuilder {
1440 ApplyOptionsBuilder::default()
1441 }
1442}
1443
1444/// Creates a new builder for [`ApplyOptions`].
1445///
1446/// This provides a classic builder pattern for constructing an [`ApplyOptions`] struct,
1447/// which can be useful when the configuration is built conditionally or comes from
1448/// multiple sources.
1449///
1450/// # Examples
1451///
1452/// ```
1453/// use mpatch::ApplyOptions;
1454///
1455/// let mut builder = ApplyOptions::builder();
1456/// let is_dry_run = true;
1457///
1458/// if is_dry_run {
1459/// builder = builder.dry_run(true);
1460/// }
1461///
1462/// let options = builder.fuzz_factor(0.8).build();
1463///
1464/// assert_eq!(options.dry_run, true);
1465/// assert_eq!(options.fuzz_factor, 0.8);
1466/// ```
1467#[derive(Debug, Clone, Copy)]
1468pub struct ApplyOptionsBuilder {
1469 dry_run: Option<bool>,
1470 fuzz_factor: Option<f32>,
1471}
1472
1473impl Default for ApplyOptionsBuilder {
1474 /// Creates a new, empty `ApplyOptionsBuilder`.
1475 ///
1476 /// All options are initially unset and will fall back to the defaults
1477 /// defined in [`ApplyOptions::default()`] when `build()` is called.
1478 ///
1479 /// # Returns
1480 ///
1481 /// A new, empty [`ApplyOptionsBuilder`] instance.
1482 ///
1483 /// # Examples
1484 ///
1485 /// ```
1486 /// use mpatch::ApplyOptionsBuilder;
1487 /// let builder = ApplyOptionsBuilder::default();
1488 /// let options = builder.build();
1489 /// assert_eq!(options.dry_run, false);
1490 /// ```
1491 fn default() -> Self {
1492 Self {
1493 dry_run: None,
1494 fuzz_factor: None,
1495 }
1496 }
1497}
1498
1499impl ApplyOptionsBuilder {
1500 /// Sets the `dry_run` flag for the patch operation.
1501 ///
1502 /// If `true`, no files will be modified. Instead, a diff of the proposed
1503 /// changes will be generated and returned in [`PatchResult`]. This is useful
1504 /// for previewing changes before they are applied.
1505 ///
1506 /// # Arguments
1507 ///
1508 /// * `dry_run` - The boolean value to set.
1509 ///
1510 /// # Returns
1511 ///
1512 /// The updated [`ApplyOptionsBuilder`] instance.
1513 ///
1514 /// # Examples
1515 ///
1516 /// ```
1517 /// # use mpatch::ApplyOptions;
1518 /// let options = ApplyOptions::builder()
1519 /// .dry_run(true) // Enable dry-run mode
1520 /// .build();
1521 /// assert!(options.dry_run);
1522 /// ```
1523 pub fn dry_run(mut self, dry_run: bool) -> Self {
1524 self.dry_run = Some(dry_run);
1525 self
1526 }
1527
1528 /// Sets the similarity threshold for fuzzy matching.
1529 ///
1530 /// The `fuzz_factor` is a value between 0.0 and 1.0 that determines how
1531 /// closely a block of text in the target file must match a hunk's context
1532 /// to be considered a "fuzzy match". A higher value requires a closer match.
1533 /// Setting it to `0.0` disables fuzzy matching entirely, requiring an exact match.
1534 ///
1535 /// # Arguments
1536 ///
1537 /// * `fuzz_factor` - The float value to set.
1538 ///
1539 /// # Returns
1540 ///
1541 /// The updated [`ApplyOptionsBuilder`] instance.
1542 ///
1543 /// # Examples
1544 ///
1545 /// ```
1546 /// # use mpatch::ApplyOptions;
1547 /// // Require a very high similarity (90%) for a fuzzy match to be accepted.
1548 /// let options = ApplyOptions::builder().fuzz_factor(0.9).build();
1549 /// assert_eq!(options.fuzz_factor, 0.9);
1550 /// ```
1551 pub fn fuzz_factor(mut self, fuzz_factor: f32) -> Self {
1552 self.fuzz_factor = Some(fuzz_factor);
1553 self
1554 }
1555
1556 /// Builds the [`ApplyOptions`] struct from the builder's configuration.
1557 ///
1558 /// This method consumes the builder and returns a final [`ApplyOptions`] instance.
1559 /// Any options not explicitly set on the builder will fall back to their
1560 /// default values.
1561 ///
1562 /// # Returns
1563 ///
1564 /// The finalized [`ApplyOptions`] instance.
1565 ///
1566 /// # Examples
1567 ///
1568 /// ```
1569 /// # use mpatch::ApplyOptions;
1570 /// let options = ApplyOptions::builder()
1571 /// .dry_run(true) // Finalize the configuration
1572 /// .fuzz_factor(0.8)
1573 /// .build();
1574 ///
1575 /// assert!(options.dry_run);
1576 /// assert_eq!(options.fuzz_factor, 0.8);
1577 /// ```
1578 pub fn build(self) -> ApplyOptions {
1579 let default = ApplyOptions::default();
1580 ApplyOptions {
1581 dry_run: self.dry_run.unwrap_or(default.dry_run),
1582 fuzz_factor: self.fuzz_factor.unwrap_or(default.fuzz_factor),
1583 }
1584 }
1585}
1586
1587/// The result of an [`apply_patch_to_file()`] operation.
1588///
1589/// This struct is returned when a patch is applied to the filesystem. It contains
1590/// a detailed report of the outcome for each hunk and, if a dry run was performed,
1591/// a diff of the proposed changes.
1592///
1593/// # Examples
1594///
1595/// ````
1596/// # use mpatch::{parse_single_patch, apply_patch_to_file, ApplyOptions};
1597/// # use std::fs;
1598/// # use tempfile::tempdir;
1599/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1600/// let dir = tempdir()?;
1601/// let file_path = dir.path().join("test.txt");
1602/// fs::write(&file_path, "line one\n")?;
1603///
1604/// let diff = r#"
1605/// ```diff
1606/// --- a/test.txt
1607/// +++ b/test.txt
1608/// @@ -1 +1 @@
1609/// -line one
1610/// +line 1
1611/// ```
1612/// "#;
1613/// let patch = parse_single_patch(diff)?;
1614///
1615/// // Perform a dry run to get a diff.
1616/// let options = ApplyOptions::dry_run();
1617/// let result = apply_patch_to_file(&patch, dir.path(), options)?;
1618///
1619/// // Check the report.
1620/// assert!(result.report.all_applied_cleanly());
1621///
1622/// // Inspect the generated diff.
1623/// assert!(result.diff.is_some());
1624/// println!("Proposed changes:\n{}", result.diff.unwrap());
1625/// # Ok(())
1626/// # }
1627/// ````
1628#[derive(Debug, Clone, PartialEq)]
1629pub struct PatchResult {
1630 /// Detailed results for each hunk within the patch operation.
1631 ///
1632 /// # Examples
1633 ///
1634 /// ```
1635 /// # use mpatch::{PatchResult, ApplyResult};
1636 /// # let result = PatchResult { report: ApplyResult { hunk_results: vec![] }, diff: None };
1637 /// assert!(result.report.all_applied_cleanly());
1638 /// ```
1639 pub report: ApplyResult,
1640 /// The unified diff of the proposed changes. This is only populated
1641 /// when `dry_run` was set to `true` in [`ApplyOptions`].
1642 ///
1643 /// # Examples
1644 ///
1645 /// ```
1646 /// # use mpatch::{PatchResult, ApplyResult};
1647 /// # let result = PatchResult { report: ApplyResult { hunk_results: vec![] }, diff: Some("--- a/file\n+++ b/file\n".to_string()) };
1648 /// if let Some(diff_text) = &result.diff {
1649 /// println!("Proposed diff:\n{}", diff_text);
1650 /// }
1651 /// ```
1652 pub diff: Option<String>,
1653}
1654
1655/// The result of an in-memory patch operation.
1656///
1657/// This struct is returned by functions like [`apply_patch_to_content()`] and
1658/// [`apply_patch_to_lines()`]. It contains the newly generated content as a string,
1659/// along with a detailed report of the outcome for each hunk.
1660///
1661/// # Examples
1662///
1663/// ````rust
1664/// # use mpatch::{parse_single_patch, apply_patch_to_content, ApplyOptions};
1665/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1666/// let original_content = "line one\n";
1667/// let diff = r#"
1668/// ```diff
1669/// --- a/test.txt
1670/// +++ b/test.txt
1671/// @@ -1 +1 @@
1672/// -line one
1673/// +line 1
1674/// ```
1675/// "#;
1676/// let patch = parse_single_patch(diff)?;
1677/// let options = ApplyOptions::new();
1678///
1679/// let result = apply_patch_to_content(&patch, Some(original_content), &options);
1680///
1681/// assert!(result.report.all_applied_cleanly());
1682/// assert_eq!(result.new_content, "line 1\n");
1683/// # Ok(())
1684/// # }
1685/// ````
1686#[derive(Debug, Clone, PartialEq)]
1687pub struct InMemoryResult {
1688 /// The new content after applying the patch.
1689 ///
1690 /// # Examples
1691 ///
1692 /// ```
1693 /// # use mpatch::{InMemoryResult, ApplyResult};
1694 /// # let result = InMemoryResult { new_content: "new text\n".to_string(), report: ApplyResult { hunk_results: vec![] } };
1695 /// assert_eq!(result.new_content, "new text\n");
1696 /// ```
1697 pub new_content: String,
1698 /// Detailed results for each hunk within the patch operation.
1699 ///
1700 /// # Examples
1701 ///
1702 /// ```
1703 /// # use mpatch::{InMemoryResult, ApplyResult};
1704 /// # let result = InMemoryResult { new_content: String::new(), report: ApplyResult { hunk_results: vec![] } };
1705 /// assert!(result.report.all_applied_cleanly());
1706 /// ```
1707 pub report: ApplyResult,
1708}
1709
1710/// Contains detailed results for each hunk within a patch operation.
1711///
1712/// This struct provides a granular report on the outcome of a patch application.
1713/// It is a key component of both [`PatchResult`] and [`InMemoryResult`]. You can
1714/// use its methods like [`all_applied_cleanly()`](ApplyResult::all_applied_cleanly) for a
1715/// high-level summary or [`failures()`](ApplyResult::failures) to inspect specific issues.
1716///
1717/// # Examples
1718///
1719/// ````rust
1720/// # use mpatch::{parse_single_patch, apply_patch_to_content, ApplyOptions, HunkApplyError};
1721/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1722/// let original_content = "line 1\n";
1723/// // This patch will fail because the context is wrong.
1724/// let diff = r#"
1725/// ```diff
1726/// --- a/test.txt
1727/// +++ b/test.txt
1728/// @@ -1 +1 @@
1729/// -WRONG CONTEXT
1730/// +line 1
1731/// ```
1732/// "#;
1733/// let patch = parse_single_patch(diff)?;
1734/// let options = ApplyOptions::exact();
1735///
1736/// let result = apply_patch_to_content(&patch, Some(original_content), &options);
1737/// let report = result.report; // This is the ApplyResult
1738///
1739/// assert!(!report.all_applied_cleanly());
1740/// assert_eq!(report.failure_count(), 1);
1741///
1742/// let failure = &report.failures()[0];
1743/// assert_eq!(failure.hunk_index, 1);
1744/// assert!(matches!(failure.reason, HunkApplyError::ContextNotFound));
1745/// # Ok(())
1746/// # }
1747/// ````
1748#[derive(Debug, Clone, PartialEq)]
1749pub struct ApplyResult {
1750 /// A list of statuses, one for each hunk in the original patch.
1751 ///
1752 /// # Examples
1753 ///
1754 /// ```
1755 /// # use mpatch::{ApplyResult, HunkApplyStatus};
1756 /// # let report = ApplyResult { hunk_results: vec![HunkApplyStatus::SkippedNoChanges] };
1757 /// assert_eq!(report.hunk_results.len(), 1);
1758 /// ```
1759 pub hunk_results: Vec<HunkApplyStatus>,
1760}
1761
1762/// Details about a hunk that failed to apply.
1763///
1764/// This struct is returned by [`ApplyResult::failures()`] and provides a convenient
1765/// way to inspect which hunk failed and for what reason.
1766///
1767/// # Examples
1768///
1769/// ````rust
1770/// # use mpatch::{parse_single_patch, apply_patch_to_content, ApplyOptions, HunkApplyError, HunkFailure};
1771/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1772/// let original_content = "line 1\n";
1773/// let diff = r#"
1774/// ```diff
1775/// --- a/test.txt
1776/// +++ b/test.txt
1777/// @@ -1 +1 @@
1778/// -WRONG CONTEXT
1779/// +line 1
1780/// ```
1781/// "#;
1782/// let patch = parse_single_patch(diff)?;
1783/// let options = ApplyOptions::exact();
1784///
1785/// let result = apply_patch_to_content(&patch, Some(original_content), &options);
1786/// let failures: Vec<HunkFailure> = result.report.failures();
1787///
1788/// assert_eq!(failures.len(), 1);
1789/// let failure = &failures[0];
1790///
1791/// assert_eq!(failure.hunk_index, 1); // 1-based index
1792/// assert!(matches!(failure.reason, HunkApplyError::ContextNotFound));
1793/// # Ok(())
1794/// # }
1795/// ````
1796#[derive(Debug, Clone, PartialEq)]
1797pub struct HunkFailure {
1798 /// The 1-based index of the hunk that failed.
1799 ///
1800 /// # Examples
1801 ///
1802 /// ```
1803 /// # use mpatch::{HunkFailure, HunkApplyError};
1804 /// # let failure = HunkFailure { hunk_index: 1, reason: HunkApplyError::ContextNotFound };
1805 /// assert_eq!(failure.hunk_index, 1);
1806 /// ```
1807 pub hunk_index: usize,
1808 /// The reason for the failure.
1809 ///
1810 /// # Examples
1811 ///
1812 /// ```
1813 /// # use mpatch::{HunkFailure, HunkApplyError};
1814 /// # let failure = HunkFailure { hunk_index: 1, reason: HunkApplyError::ContextNotFound };
1815 /// assert!(matches!(failure.reason, HunkApplyError::ContextNotFound));
1816 /// ```
1817 pub reason: HunkApplyError,
1818}
1819
1820impl ApplyResult {
1821 /// Checks if all hunks in the patch were applied successfully or skipped.
1822 ///
1823 /// Returns `false` if any hunk failed to apply.
1824 ///
1825 /// # Returns
1826 ///
1827 /// `true` if all hunks applied cleanly, `false` otherwise.
1828 ///
1829 /// # Examples
1830 ///
1831 /// ```
1832 /// # use mpatch::{ApplyResult, HunkApplyStatus, HunkApplyError, HunkFailure, HunkLocation, MatchType};
1833 /// let successful_result = ApplyResult {
1834 /// hunk_results: vec![
1835 /// HunkApplyStatus::Applied { location: HunkLocation { start_index: 0, length: 1 }, match_type: MatchType::Exact, replaced_lines: vec!["old".to_string()] },
1836 /// HunkApplyStatus::SkippedNoChanges
1837 /// ],
1838 /// };
1839 /// assert!(successful_result.all_applied_cleanly());
1840 ///
1841 /// let failed_result = ApplyResult {
1842 /// hunk_results: vec![
1843 /// HunkApplyStatus::Applied { location: HunkLocation { start_index: 0, length: 1 }, match_type: MatchType::Exact, replaced_lines: vec!["old".to_string()] },
1844 /// HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
1845 /// ],
1846 /// };
1847 /// assert!(!failed_result.all_applied_cleanly());
1848 /// ```
1849 pub fn all_applied_cleanly(&self) -> bool {
1850 self.hunk_results
1851 .iter()
1852 .all(|r| !matches!(r, HunkApplyStatus::Failed(_)))
1853 }
1854
1855 /// Returns a list of all hunks that failed to apply, along with their index.
1856 ///
1857 /// This provides a more convenient way to inspect failures than iterating
1858 /// through [`hunk_results`](ApplyResult::hunk_results) manually.
1859 ///
1860 /// # Returns
1861 ///
1862 /// A vector of [`HunkFailure`] objects representing the failed hunks.
1863 ///
1864 /// # Examples
1865 ///
1866 /// ```
1867 /// # use mpatch::{ApplyResult, HunkApplyStatus, HunkApplyError, HunkFailure, HunkLocation, MatchType};
1868 /// let failed_result = ApplyResult {
1869 /// hunk_results: vec![
1870 /// // The first hunk applied successfully.
1871 /// HunkApplyStatus::Applied { location: HunkLocation { start_index: 0, length: 1 }, match_type: MatchType::Exact, replaced_lines: vec!["old".to_string()] },
1872 /// HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
1873 /// ],
1874 /// };
1875 /// let failures = failed_result.failures();
1876 /// assert_eq!(failures.len(), 1);
1877 /// assert_eq!(failures[0], HunkFailure {
1878 /// hunk_index: 2, // 1-based index
1879 /// reason: HunkApplyError::ContextNotFound,
1880 /// });
1881 /// ```
1882 pub fn failures(&self) -> Vec<HunkFailure> {
1883 self.hunk_results
1884 .iter()
1885 .enumerate()
1886 .filter_map(|(i, status)| {
1887 if let HunkApplyStatus::Failed(reason) = status {
1888 Some(HunkFailure {
1889 hunk_index: i + 1,
1890 reason: reason.clone(),
1891 })
1892 } else {
1893 None
1894 }
1895 })
1896 .collect()
1897 }
1898}
1899
1900/// The result of applying a batch of patches to a directory.
1901///
1902/// This struct is returned by [`apply_patches_to_dir()`] and aggregates the results
1903/// for each individual patch operation. It allows you to check for "hard" errors
1904/// (like I/O issues) separately from "soft" errors (like a hunk failing to apply).
1905///
1906/// # Examples
1907///
1908/// ````rust
1909/// # use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions};
1910/// # use std::fs;
1911/// # use tempfile::tempdir;
1912/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1913/// let dir = tempdir()?;
1914/// fs::write(dir.path().join("file1.txt"), "foo\n")?;
1915/// fs::write(dir.path().join("file2.txt"), "baz\n")?;
1916///
1917/// let diff = r#"
1918/// ```diff
1919/// --- a/file1.txt
1920/// +++ b/file1.txt
1921/// @@ -1 +1 @@
1922/// -foo
1923/// +bar
1924/// --- a/file2.txt
1925/// +++ b/file2.txt
1926/// @@ -1 +1 @@
1927/// -WRONG
1928/// +qux
1929/// ```
1930/// "#;
1931/// let patches = parse_auto(diff)?;
1932/// let options = ApplyOptions::exact();
1933///
1934/// let batch_result = apply_patches_to_dir(&patches, dir.path(), options);
1935///
1936/// // The overall batch succeeded (no I/O errors).
1937/// assert!(batch_result.all_succeeded());
1938///
1939/// // But we can inspect individual results for partial failures.
1940/// for (path, result) in &batch_result.results {
1941/// let patch_result = result.as_ref().unwrap();
1942/// if path.to_str() == Some("file1.txt") {
1943/// assert!(patch_result.report.all_applied_cleanly());
1944/// } else {
1945/// assert!(!patch_result.report.all_applied_cleanly());
1946/// }
1947/// }
1948/// # Ok(())
1949/// # }
1950/// ````
1951#[derive(Debug)]
1952pub struct BatchResult {
1953 /// A list of results for each patch operation attempted.
1954 /// Each entry is a tuple of the target file path and the result of the operation.
1955 ///
1956 /// # Examples
1957 ///
1958 /// ```
1959 /// # use mpatch::BatchResult;
1960 /// # let batch = BatchResult { results: vec![] };
1961 /// assert!(batch.results.is_empty());
1962 /// ```
1963 pub results: Vec<(PathBuf, Result<PatchResult, PatchError>)>,
1964}
1965
1966impl BatchResult {
1967 /// Checks if all patches in the batch were applied without "hard" errors (like I/O errors).
1968 /// This does *not* check if all hunks were applied cleanly. For that, you must
1969 /// inspect the individual `PatchResult` objects.
1970 ///
1971 /// # Returns
1972 ///
1973 /// `true` if all patches succeeded without hard errors, `false` otherwise.
1974 ///
1975 /// # Examples
1976 ///
1977 /// ````rust
1978 /// # use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions};
1979 /// # use std::fs;
1980 /// # use tempfile::tempdir;
1981 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1982 /// let dir = tempdir()?;
1983 /// fs::write(dir.path().join("file1.txt"), "foo\n")?;
1984 /// // Note: file2.txt does not exist, which will cause a hard error.
1985 ///
1986 /// let diff = r#"
1987 /// ```diff
1988 /// --- a/file1.txt
1989 /// +++ b/file1.txt
1990 /// @@ -1 +1 @@
1991 /// -foo
1992 /// +bar
1993 /// --- a/file2.txt
1994 /// +++ b/file2.txt
1995 /// @@ -1 +1 @@
1996 /// -baz
1997 /// +qux
1998 /// ```
1999 /// "#;
2000 /// let patches = parse_auto(diff)?;
2001 /// let options = ApplyOptions::new();
2002 ///
2003 /// let batch_result = apply_patches_to_dir(&patches, dir.path(), options);
2004 ///
2005 /// // The batch did not fully succeed because of the missing file.
2006 /// assert!(!batch_result.all_succeeded());
2007 /// # Ok(())
2008 /// # }
2009 /// ````
2010 pub fn all_succeeded(&self) -> bool {
2011 self.results.iter().all(|(_, res)| res.is_ok())
2012 }
2013
2014 /// Returns a list of all operations that resulted in a "hard" error (e.g., I/O).
2015 ///
2016 /// This method is useful for isolating critical failures that prevented a patch
2017 /// from being attempted, such as file system errors, permission issues, or
2018 /// security violations. It filters the results to only include `Err` variants,
2019 /// providing a direct way to report or handle unrecoverable problems in a batch
2020 /// run.
2021 ///
2022 /// # Returns
2023 ///
2024 /// A vector of tuples containing the file path and the associated [`PatchError`].
2025 ///
2026 /// # Examples
2027 ///
2028 /// ```rust
2029 /// # use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions, PatchError};
2030 /// # use std::fs;
2031 /// # use tempfile::tempdir;
2032 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2033 /// # let dir = tempdir()?;
2034 /// # let diff = "```diff\n--- a/missing.txt\n+++ b/missing.txt\n@@ -1 +1 @@\n-a\n+b\n```";
2035 /// # let patches = parse_auto(diff)?;
2036 /// let batch_result = apply_patches_to_dir(&patches, dir.path(), ApplyOptions::new());
2037 ///
2038 /// // Check for any hard failures in the batch.
2039 /// let failures = batch_result.hard_failures();
2040 /// assert_eq!(failures.len(), 1);
2041 /// assert_eq!(failures[0].0.to_str(), Some("missing.txt"));
2042 /// assert!(matches!(failures[0].1, PatchError::TargetNotFound(_)));
2043 /// # Ok(())
2044 /// # }
2045 /// ```
2046 pub fn hard_failures(&self) -> Vec<(&PathBuf, &PatchError)> {
2047 self.results
2048 .iter()
2049 .filter_map(|(path, res)| res.as_ref().err().map(|e| (path, e)))
2050 .collect()
2051 }
2052}
2053
2054impl ApplyResult {
2055 /// Checks if any hunk in the patch failed to apply.
2056 ///
2057 /// This is the logical opposite of [`all_applied_cleanly`](ApplyResult::all_applied_cleanly).
2058 ///
2059 /// # Returns
2060 ///
2061 /// `true` if any hunk failed to apply, `false` otherwise.
2062 ///
2063 /// # Examples
2064 ///
2065 /// ```
2066 /// # use mpatch::{ApplyResult, HunkApplyStatus, HunkApplyError, HunkLocation, MatchType};
2067 /// let failed_result = ApplyResult {
2068 /// hunk_results: vec![
2069 /// HunkApplyStatus::Applied { location: HunkLocation { start_index: 0, length: 1 }, match_type: MatchType::Exact, replaced_lines: vec!["old".to_string()] },
2070 /// HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
2071 /// ],
2072 /// };
2073 /// assert!(failed_result.has_failures());
2074 ///
2075 /// let successful_result = ApplyResult {
2076 /// hunk_results: vec![ HunkApplyStatus::SkippedNoChanges ],
2077 /// };
2078 /// assert!(!successful_result.has_failures());
2079 /// ```
2080 pub fn has_failures(&self) -> bool {
2081 !self.all_applied_cleanly()
2082 }
2083
2084 /// Returns the number of hunks that failed to apply.
2085 ///
2086 /// This is a convenience method that counts how many hunks in the `hunk_results`
2087 /// list have a status of [`HunkApplyStatus::Failed`].
2088 ///
2089 /// # Returns
2090 ///
2091 /// The number of failed hunks.
2092 ///
2093 /// # Examples
2094 ///
2095 /// ```
2096 /// # use mpatch::{ApplyResult, HunkApplyStatus, HunkApplyError, HunkLocation, MatchType};
2097 /// let result = ApplyResult {
2098 /// hunk_results: vec![
2099 /// HunkApplyStatus::Applied { location: HunkLocation { start_index: 0, length: 1 }, match_type: MatchType::Exact, replaced_lines: vec!["old".to_string()] },
2100 /// HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
2101 /// HunkApplyStatus::Failed(HunkApplyError::AmbiguousExactMatch(vec![])),
2102 /// ],
2103 /// };
2104 /// assert_eq!(result.failure_count(), 2);
2105 /// ```
2106 pub fn failure_count(&self) -> usize {
2107 self.failures().len()
2108 }
2109
2110 /// Returns the number of hunks that were applied successfully or skipped.
2111 ///
2112 /// This method counts how many hunks in the `hunk_results` list have a status
2113 /// of either [`HunkApplyStatus::Applied`] or [`HunkApplyStatus::SkippedNoChanges`].
2114 ///
2115 /// # Returns
2116 ///
2117 /// The number of successful or skipped hunks.
2118 ///
2119 /// # Examples
2120 ///
2121 /// ```
2122 /// # use mpatch::{ApplyResult, HunkApplyStatus, HunkApplyError, HunkLocation, MatchType};
2123 /// let result = ApplyResult {
2124 /// hunk_results: vec![
2125 /// HunkApplyStatus::Applied { location: HunkLocation { start_index: 0, length: 1 }, match_type: MatchType::Exact, replaced_lines: vec!["old".to_string()] },
2126 /// HunkApplyStatus::SkippedNoChanges,
2127 /// HunkApplyStatus::Failed(HunkApplyError::ContextNotFound),
2128 /// ],
2129 /// };
2130 /// assert_eq!(result.success_count(), 2);
2131 /// ```
2132 pub fn success_count(&self) -> usize {
2133 self.hunk_results.len() - self.failure_count()
2134 }
2135}
2136
2137// --- Data Structures ---
2138
2139/// Represents a single hunk of changes within a patch.
2140///
2141/// Structurally, this models a hunk from a Unified Diff (the `@@ ... @@` blocks),
2142/// storing lines prefixed with `+`, `-`, or space. However, it serves as the
2143/// universal internal representation for all patch formats in `mpatch`. This
2144/// abstraction allows the matching engine to operate identically regardless
2145/// of whether the input was a formal `.patch` file or a snippet of
2146/// conflict markers.
2147///
2148/// - **Unified Diffs:** Parsed directly.
2149/// - **Conflict Markers:** Converted into a `Hunk` where the "old" block becomes
2150/// deletions and the "new" block becomes additions.
2151///
2152/// You typically get `Hunk` objects as part of a [`Patch`] after parsing a diff.
2153///
2154/// # Examples
2155///
2156/// ````rust
2157/// # use mpatch::parse_single_patch;
2158/// let diff = r#"
2159/// ```diff
2160/// --- a/file.txt
2161/// +++ b/file.txt
2162/// @@ -10,2 +10,2 @@
2163/// context line
2164/// -removed line
2165/// +added line
2166/// ```
2167/// "#;
2168/// let patch = parse_single_patch(diff).unwrap();
2169/// let hunk = &patch.hunks[0];
2170///
2171/// assert_eq!(hunk.old_start_line, Some(10));
2172/// assert_eq!(hunk.removed_lines(), vec!["removed line"]);
2173/// assert_eq!(hunk.added_lines(), vec!["added line"]);
2174///
2175/// // You can convert the hunk back to a unified diff string:
2176/// assert_eq!(hunk.to_string(), "@@ -10,2 +10,2 @@\n context line\n-removed line\n+added line\n");
2177/// ````
2178#[derive(Debug, Clone, PartialEq, Eq)]
2179pub struct Hunk {
2180 /// The raw lines of the hunk, each prefixed with ' ', '+', or '-'.
2181 ///
2182 /// This vector stores the content exactly as it would appear in a Unified Diff body.
2183 /// Lines starting with ` ` are context, `-` are deletions, and `+` are additions.
2184 ///
2185 /// When parsing Conflict Markers, `mpatch` synthesizes these lines: the "before"
2186 /// block becomes `-` lines, and the "after" block becomes `+` lines.
2187 ///
2188 /// # Examples
2189 ///
2190 /// ```
2191 /// # use mpatch::{parse_single_patch, Hunk};
2192 /// # let diff = "```diff\n--- a/f\n+++ b/f\n@@ -1,2 +1,2\n-a\n+b\n```";
2193 /// # let patch = parse_single_patch(diff).unwrap();
2194 /// let hunk = &patch.hunks[0];
2195 ///
2196 /// // Iterate over the raw lines
2197 /// for line in &hunk.lines {
2198 /// if line.starts_with('+') {
2199 /// println!("Added line: {}", &line[1..]);
2200 /// }
2201 /// }
2202 /// ```
2203 pub lines: Vec<String>,
2204 /// The starting line number in the original file (1-based).
2205 ///
2206 /// This corresponds to the `l` in the `@@ -l,s ...` header of a unified diff.
2207 /// In `mpatch`, this value is primarily used as a **hint** to resolve ambiguity.
2208 /// If the context matches in multiple places, the location closest to this line
2209 /// is chosen.
2210 ///
2211 /// This may be `None` if the patch source (like Conflict Markers) did not provide line numbers.
2212 ///
2213 /// # Examples
2214 ///
2215 /// ```
2216 /// # use mpatch::{Hunk};
2217 /// let hunk = Hunk {
2218 /// lines: vec!["-old".to_string()],
2219 /// old_start_line: Some(10), // Hint: look near line 10
2220 /// new_start_line: Some(10),
2221 /// };
2222 /// ```
2223 pub old_start_line: Option<usize>,
2224 /// The starting line number in the new file (1-based).
2225 ///
2226 /// This corresponds to the `l` in the `@@ ... +l,s @@` header of a unified diff.
2227 /// It represents the intended location in the resulting file.
2228 ///
2229 /// This may be `None` if the patch source did not provide line numbers.
2230 ///
2231 /// # Examples
2232 ///
2233 /// ```
2234 /// # use mpatch::{Hunk};
2235 /// let hunk = Hunk {
2236 /// lines: vec!["+new".to_string()],
2237 /// old_start_line: Some(10),
2238 /// new_start_line: Some(12), // Lines shifted down by 2
2239 /// };
2240 /// ```
2241 pub new_start_line: Option<usize>,
2242}
2243
2244impl Hunk {
2245 /// Creates a new `Hunk` that reverses the changes in this one.
2246 ///
2247 /// Additions become deletions, and deletions become additions. Context lines
2248 /// remain unchanged. The old and new line number hints are swapped.
2249 ///
2250 /// # Returns
2251 ///
2252 /// A new [`Hunk`] with additions and deletions swapped.
2253 ///
2254 /// # Examples
2255 ///
2256 /// ```
2257 /// # use mpatch::Hunk;
2258 /// let hunk = Hunk {
2259 /// lines: vec![
2260 /// " context".to_string(),
2261 /// "-deleted".to_string(),
2262 /// "+added".to_string(),
2263 /// ],
2264 /// old_start_line: Some(10),
2265 /// new_start_line: Some(12),
2266 /// };
2267 /// let inverted_hunk = hunk.invert();
2268 /// assert_eq!(inverted_hunk.lines, vec![
2269 /// " context".to_string(),
2270 /// "+deleted".to_string(),
2271 /// "-added".to_string(),
2272 /// ]);
2273 /// assert_eq!(inverted_hunk.old_start_line, Some(12));
2274 /// assert_eq!(inverted_hunk.new_start_line, Some(10));
2275 /// ```
2276 pub fn invert(&self) -> Hunk {
2277 let inverted_lines = self
2278 .lines
2279 .iter()
2280 .map(|line| {
2281 if let Some(stripped) = line.strip_prefix('+') {
2282 format!("-{}", stripped)
2283 } else if let Some(stripped) = line.strip_prefix('-') {
2284 format!("+{}", stripped)
2285 } else {
2286 line.clone()
2287 }
2288 })
2289 .collect();
2290
2291 Hunk {
2292 lines: inverted_lines,
2293 old_start_line: self.new_start_line,
2294 new_start_line: self.old_start_line,
2295 }
2296 }
2297
2298 /// Extracts the lines that need to be matched in the target file.
2299 ///
2300 /// This includes context lines (starting with ' ') and deletion lines
2301 /// (starting with '-'). The leading character is stripped. These lines form
2302 /// the "search pattern" that `mpatch` looks for in the target file.
2303 ///
2304 /// # Returns
2305 ///
2306 /// A vector of string slices representing the match block.
2307 ///
2308 /// # Examples
2309 ///
2310 /// ```
2311 /// # use mpatch::Hunk;
2312 /// let hunk = Hunk {
2313 /// lines: vec![
2314 /// " context".to_string(),
2315 /// "-deleted".to_string(),
2316 /// "+added".to_string(),
2317 /// ],
2318 /// old_start_line: None,
2319 /// new_start_line: None,
2320 /// };
2321 /// assert_eq!(hunk.get_match_block(), vec!["context", "deleted"]);
2322 /// ```
2323 pub fn get_match_block(&self) -> Vec<&str> {
2324 self.lines
2325 .iter()
2326 .filter(|l| !l.starts_with('+'))
2327 .map(|l| &l[1..])
2328 .collect()
2329 }
2330
2331 /// Extracts the lines that will replace the matched block in the target file.
2332 ///
2333 /// This includes context lines (starting with ' ') and addition lines
2334 /// (starting with '+'). The leading character is stripped. This is the
2335 /// content that will be "spliced" into the file.
2336 ///
2337 /// # Returns
2338 ///
2339 /// A vector of string slices representing the replace block.
2340 ///
2341 /// # Examples
2342 ///
2343 /// ```
2344 /// # use mpatch::Hunk;
2345 /// let hunk = Hunk {
2346 /// lines: vec![
2347 /// " context".to_string(),
2348 /// "-deleted".to_string(),
2349 /// "+added".to_string(),
2350 /// ],
2351 /// old_start_line: None,
2352 /// new_start_line: None,
2353 /// };
2354 /// assert_eq!(hunk.get_replace_block(), vec!["context", "added"]);
2355 /// ```
2356 pub fn get_replace_block(&self) -> Vec<&str> {
2357 self.lines
2358 .iter()
2359 .filter(|l| !l.starts_with('-'))
2360 .map(|l| &l[1..])
2361 .collect()
2362 }
2363
2364 /// Extracts the context lines from the hunk.
2365 ///
2366 /// These are lines that start with ' ' and are stripped of the prefix.
2367 ///
2368 /// # Returns
2369 ///
2370 /// A vector of string slices representing the context lines.
2371 ///
2372 /// # Examples
2373 ///
2374 /// ```
2375 /// # use mpatch::Hunk;
2376 /// let hunk = Hunk {
2377 /// lines: vec![
2378 /// " context".to_string(),
2379 /// "-deleted".to_string(),
2380 /// "+added".to_string(),
2381 /// ],
2382 /// old_start_line: None,
2383 /// new_start_line: None,
2384 /// };
2385 /// assert_eq!(hunk.context_lines(), vec!["context"]);
2386 /// ```
2387 pub fn context_lines(&self) -> Vec<&str> {
2388 self.lines
2389 .iter()
2390 .filter(|l| l.starts_with(' '))
2391 .map(|l| &l[1..])
2392 .collect()
2393 }
2394
2395 /// Extracts the added lines from the hunk.
2396 ///
2397 /// These are lines that start with '+' and are stripped of the prefix.
2398 ///
2399 /// # Returns
2400 ///
2401 /// A vector of string slices representing the added lines.
2402 ///
2403 /// # Examples
2404 ///
2405 /// ```
2406 /// # use mpatch::Hunk;
2407 /// let hunk = Hunk {
2408 /// lines: vec![
2409 /// " context".to_string(),
2410 /// "-deleted".to_string(),
2411 /// "+added".to_string(),
2412 /// ],
2413 /// old_start_line: None,
2414 /// new_start_line: None,
2415 /// };
2416 /// assert_eq!(hunk.added_lines(), vec!["added"]);
2417 /// ```
2418 pub fn added_lines(&self) -> Vec<&str> {
2419 self.lines
2420 .iter()
2421 .filter(|l| l.starts_with('+'))
2422 .map(|l| &l[1..])
2423 .collect()
2424 }
2425
2426 /// Extracts the removed lines from the hunk.
2427 ///
2428 /// These are lines that start with '-' and are stripped of the prefix.
2429 ///
2430 /// # Returns
2431 ///
2432 /// A vector of string slices representing the removed lines.
2433 ///
2434 /// # Examples
2435 ///
2436 /// ```
2437 /// # use mpatch::Hunk;
2438 /// let hunk = Hunk {
2439 /// lines: vec![
2440 /// " context".to_string(),
2441 /// "-deleted".to_string(),
2442 /// "+added".to_string(),
2443 /// ],
2444 /// old_start_line: None,
2445 /// new_start_line: None,
2446 /// };
2447 /// assert_eq!(hunk.removed_lines(), vec!["deleted"]);
2448 /// ```
2449 pub fn removed_lines(&self) -> Vec<&str> {
2450 self.lines
2451 .iter()
2452 .filter(|l| l.starts_with('-'))
2453 .map(|l| &l[1..])
2454 .collect()
2455 }
2456
2457 /// Checks if the hunk contains any effective changes (additions or deletions).
2458 ///
2459 /// A hunk with only context lines has no changes and can be skipped.
2460 ///
2461 /// # Returns
2462 ///
2463 /// `true` if the hunk has additions or deletions, `false` otherwise.
2464 ///
2465 /// # Examples
2466 ///
2467 /// ```
2468 /// # use mpatch::Hunk;
2469 /// let hunk_with_changes = Hunk {
2470 /// lines: vec![ "+ a".to_string() ],
2471 /// old_start_line: None,
2472 /// new_start_line: None,
2473 /// };
2474 /// assert!(hunk_with_changes.has_changes());
2475 ///
2476 /// let hunk_without_changes = Hunk {
2477 /// lines: vec![ " a".to_string() ],
2478 /// old_start_line: None,
2479 /// new_start_line: None,
2480 /// };
2481 /// assert!(!hunk_without_changes.has_changes());
2482 /// ```
2483 pub fn has_changes(&self) -> bool {
2484 self.lines.iter().any(|l| l.starts_with(['+', '-']))
2485 }
2486}
2487
2488impl std::fmt::Display for Hunk {
2489 /// Formats the hunk into a valid unified diff hunk block.
2490 ///
2491 /// This generates the `@@ ... @@` header based on the start lines and the
2492 /// count of lines in the `lines` vector, followed by the content. This allows
2493 /// any `Hunk` (even those from Conflict Markers) to be serialized as standard diffs.
2494 ///
2495 /// If `old_start_line` or `new_start_line` are `None`, they default to `1` in the output.
2496 ///
2497 /// # Arguments
2498 ///
2499 /// * `f` - The formatter to write the output to.
2500 ///
2501 /// # Returns
2502 ///
2503 /// `Ok(())` if the formatting was successful.
2504 ///
2505 /// # Errors
2506 ///
2507 /// Returns `Err(std::fmt::Error)` if writing to the formatter fails.
2508 ///
2509 /// # Examples
2510 ///
2511 /// ```
2512 /// # use mpatch::Hunk;
2513 /// let hunk = Hunk {
2514 /// lines: vec![
2515 /// " context".to_string(),
2516 /// "-deleted".to_string(),
2517 /// "+added".to_string(),
2518 /// ],
2519 /// old_start_line: Some(10),
2520 /// new_start_line: Some(12),
2521 /// };
2522 /// let expected_str = "@@ -10,2 +12,2 @@\n context\n-deleted\n+added\n";
2523 /// assert_eq!(hunk.to_string(), expected_str);
2524 /// ```
2525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2526 let (old_len, new_len) = self.lines.iter().fold((0, 0), |(old, new), line| {
2527 if line.starts_with('+') {
2528 (old, new + 1)
2529 } else if line.starts_with('-') {
2530 (old + 1, new)
2531 } else {
2532 (old + 1, new + 1)
2533 }
2534 });
2535 let old_start = self.old_start_line.unwrap_or(1);
2536 let new_start = self.new_start_line.unwrap_or(1);
2537
2538 writeln!(
2539 f,
2540 "@@ -{},{} +{},{} @@",
2541 old_start, old_len, new_start, new_len
2542 )?;
2543
2544 for line in &self.lines {
2545 writeln!(f, "{}", line)?;
2546 }
2547 Ok(())
2548 }
2549}
2550
2551/// Represents the location where a hunk should be applied.
2552///
2553/// This is returned by [`find_hunk_location()`] and provides the necessary
2554/// information to manually apply a patch to a slice of lines.
2555///
2556/// # Examples
2557///
2558/// ````rust
2559/// # use mpatch::{find_hunk_location, parse_single_patch, ApplyOptions, HunkLocation, MatchType};
2560/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2561/// let original_content = "line 1\nline 2\nline 3\n";
2562/// let diff = r#"
2563/// ```diff
2564/// --- a/file.txt
2565/// +++ b/file.txt
2566/// @@ -1,3 +1,3 @@
2567/// line 1
2568/// -line 2
2569/// +line two
2570/// line 3
2571/// ```
2572/// "#;
2573/// let hunk = parse_single_patch(diff)?.hunks.remove(0);
2574/// let options = ApplyOptions::exact();
2575///
2576/// let (location, _) = find_hunk_location(&hunk, original_content, &options)?;
2577///
2578/// assert_eq!(location.start_index, 0); // 0-based index
2579/// assert_eq!(location.length, 3);
2580/// assert_eq!(location.to_string(), "line 1"); // User-friendly 1-based display
2581/// # Ok(())
2582/// # }
2583/// ````
2584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2585pub struct HunkLocation {
2586 /// The 0-based starting line index in the target content where the hunk should be applied.
2587 ///
2588 /// This index indicates the first line of the slice in the target content that
2589 /// will be replaced by the hunk's changes. You can use this along with the
2590 /// `length` field to understand the exact range of lines affected by the patch.
2591 ///
2592 /// # Examples
2593 ///
2594 /// ```
2595 /// # use mpatch::{HunkLocation};
2596 /// let location = HunkLocation { start_index: 4, length: 3 };
2597 ///
2598 /// // Note that the user-facing line number is start_index + 1.
2599 /// assert_eq!(location.start_index, 4);
2600 /// println!(
2601 /// "Patch will be applied starting at line {} (index {}).",
2602 /// location.start_index + 1,
2603 /// location.start_index
2604 /// );
2605 /// ```
2606 pub start_index: usize,
2607 /// The number of lines in the target content that will be replaced. This may
2608 /// differ from the number of lines in the hunk's "match block" when a fuzzy
2609 /// match is found.
2610 ///
2611 /// # Examples
2612 ///
2613 /// ```
2614 /// # use mpatch::HunkLocation;
2615 /// let location = HunkLocation { start_index: 0, length: 5 };
2616 /// assert_eq!(location.length, 5);
2617 /// ```
2618 pub length: usize,
2619}
2620
2621impl std::fmt::Display for HunkLocation {
2622 /// Formats the location for display, showing a user-friendly 1-based line number.
2623 ///
2624 /// This implementation provides a more intuitive, human-readable representation of the
2625 /// hunk's location. It converts the internal 0-based `start_index` into a 1-based
2626 /// line number (e.g., index `9` becomes `"line 10"`), which is the standard
2627 /// convention in text editors and log messages. This makes it easy to use
2628 /// `HunkLocation` directly in formatted strings for clear diagnostic output.
2629 ///
2630 /// # Arguments
2631 ///
2632 /// * `f` - The formatter to write the output to.
2633 ///
2634 /// # Returns
2635 ///
2636 /// `Ok(())` if the formatting was successful.
2637 ///
2638 /// # Errors
2639 ///
2640 /// Returns `Err(std::fmt::Error)` if writing to the formatter fails.
2641 ///
2642 /// # Examples
2643 ///
2644 /// ```
2645 /// # use mpatch::HunkLocation;
2646 /// let location = HunkLocation { start_index: 9, length: 3 };
2647 /// assert_eq!(location.to_string(), "line 10");
2648 /// assert_eq!(format!("Hunk applied at {}", location), "Hunk applied at line 10");
2649 /// ```
2650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2651 // Adding 1 to start_index for a more user-friendly 1-based line number.
2652 write!(f, "line {}", self.start_index + 1)
2653 }
2654}
2655
2656/// Represents all the changes to be applied to a single file.
2657///
2658/// A `Patch` contains a target file path and a list of [`Hunk`]s. It is typically
2659/// created by parsing a diff string (Unified Diff, Markdown block, or Conflict Markers)
2660/// using functions like [`parse_auto()`] or [`parse_diffs()`]. It can represent
2661/// file modifications, creations, or deletions.
2662///
2663/// It is the primary unit of work for the `apply` functions.
2664///
2665/// # Examples
2666///
2667/// ````rust
2668/// # use mpatch::parse_single_patch;
2669/// let diff = r#"
2670/// ```diff
2671/// --- a/src/main.rs
2672/// +++ b/src/main.rs
2673/// @@ -1,3 +1,3 @@
2674/// fn main() {
2675/// - println!("Hello, world!");
2676/// + println!("Hello, mpatch!");
2677/// }
2678/// ```
2679/// "#;
2680/// let patch = parse_single_patch(diff).unwrap();
2681///
2682/// assert_eq!(patch.file_path.to_str(), Some("src/main.rs"));
2683/// assert_eq!(patch.hunks.len(), 1);
2684/// assert!(patch.ends_with_newline);
2685/// ````
2686#[derive(Debug, Clone, PartialEq, Eq)]
2687pub struct Patch {
2688 /// The relative path of the file to be patched, from the target directory.
2689 ///
2690 /// This path is extracted from the `--- a/path/to/file` header in the diff.
2691 /// It's a `PathBuf`, so you can use it directly with filesystem operations
2692 /// or convert it to a string for display.
2693 ///
2694 /// # Examples
2695 ///
2696 /// ```
2697 /// # use mpatch::parse_single_patch;
2698 /// # let diff = "```diff\n--- a/src/main.rs\n+++ b/src/main.rs\n@@ -1,1 +1,1\n-a\n+b\n```";
2699 /// let patch = parse_single_patch(diff).unwrap();
2700 ///
2701 /// assert_eq!(patch.file_path.to_str(), Some("src/main.rs"));
2702 /// println!("Patch targets the file: {}", patch.file_path.display());
2703 /// ```
2704 pub file_path: PathBuf,
2705 /// A list of hunks to be applied to the file.
2706 ///
2707 /// # Examples
2708 ///
2709 /// ```
2710 /// # use mpatch::Patch;
2711 /// # let patch = Patch { file_path: std::path::PathBuf::from("f"), hunks: vec![], ends_with_newline: true };
2712 /// assert!(patch.hunks.is_empty());
2713 /// ```
2714 pub hunks: Vec<Hunk>,
2715 /// Indicates whether the file should end with a newline.
2716 /// This is determined by the presence of `\ No newline at end of file`
2717 /// in the diff.
2718 ///
2719 /// # Examples
2720 ///
2721 /// ```
2722 /// # use mpatch::Patch;
2723 /// # let patch = Patch { file_path: std::path::PathBuf::from("f"), hunks: vec![], ends_with_newline: false };
2724 /// assert_eq!(patch.ends_with_newline, false);
2725 /// ```
2726 pub ends_with_newline: bool,
2727}
2728
2729impl Patch {
2730 /// Creates a new `Patch` by comparing two texts.
2731 ///
2732 /// This function generates a unified diff between the `old_text` and `new_text`
2733 /// and then parses it into a `Patch` object. This allows `mpatch` to be used
2734 /// not just for applying patches, but also for creating them.
2735 ///
2736 /// # Arguments
2737 ///
2738 /// * `file_path` - The path to associate with the patch (e.g., `src/main.rs`).
2739 /// * `old_text` - The original text content.
2740 /// * `new_text` - The new, modified text content.
2741 /// * `context_len` - The number of context lines to include around changes.
2742 ///
2743 /// # Returns
2744 ///
2745 /// A new [`Patch`] object.
2746 ///
2747 /// # Errors
2748 ///
2749 /// Returns `Err(`[`ParseError`]`)` if the diff cannot be parsed.
2750 ///
2751 /// # Examples
2752 ///
2753 /// ```
2754 /// # use mpatch::Patch;
2755 /// let old_code = "fn main() {\n println!(\"old\");\n}\n";
2756 /// let new_code = "fn main() {\n println!(\"new\");\n}\n";
2757 ///
2758 /// let patch = Patch::from_texts("src/main.rs", old_code, new_code, 3).unwrap();
2759 ///
2760 /// assert_eq!(patch.file_path.to_str(), Some("src/main.rs"));
2761 /// assert_eq!(patch.hunks.len(), 1);
2762 /// assert_eq!(patch.hunks[0].removed_lines(), vec![" println!(\"old\");"]);
2763 /// assert_eq!(patch.hunks[0].added_lines(), vec![" println!(\"new\");"]);
2764 /// ```
2765 pub fn from_texts(
2766 file_path: impl Into<PathBuf>,
2767 old_text: &str,
2768 new_text: &str,
2769 context_len: usize,
2770 ) -> Result<Self, ParseError> {
2771 let path = file_path.into();
2772 let diff = TextDiff::from_lines(old_text, new_text);
2773 let mut hunks = Vec::new();
2774
2775 for group in diff.grouped_ops(context_len) {
2776 let mut lines = Vec::new();
2777 let mut old_start = None;
2778 let mut new_start = None;
2779
2780 if let Some(first_op) = group.first() {
2781 old_start = Some(first_op.old_range().start + 1);
2782 new_start = Some(first_op.new_range().start + 1);
2783 }
2784
2785 for op in group {
2786 match op {
2787 similar::DiffOp::Equal { old_index, len, .. } => {
2788 for i in 0..len {
2789 let line =
2790 diff.old_slices()[old_index + i].trim_end_matches(['\r', '\n']);
2791 lines.push(format!(" {}", line));
2792 }
2793 }
2794 similar::DiffOp::Delete {
2795 old_index, old_len, ..
2796 } => {
2797 for i in 0..old_len {
2798 let line =
2799 diff.old_slices()[old_index + i].trim_end_matches(['\r', '\n']);
2800 lines.push(format!("-{}", line));
2801 }
2802 }
2803 similar::DiffOp::Insert {
2804 new_index, new_len, ..
2805 } => {
2806 for i in 0..new_len {
2807 let line =
2808 diff.new_slices()[new_index + i].trim_end_matches(['\r', '\n']);
2809 lines.push(format!("+{}", line));
2810 }
2811 }
2812 similar::DiffOp::Replace {
2813 old_index,
2814 old_len,
2815 new_index,
2816 new_len,
2817 } => {
2818 for i in 0..old_len {
2819 let line =
2820 diff.old_slices()[old_index + i].trim_end_matches(['\r', '\n']);
2821 lines.push(format!("-{}", line));
2822 }
2823 for i in 0..new_len {
2824 let line =
2825 diff.new_slices()[new_index + i].trim_end_matches(['\r', '\n']);
2826 lines.push(format!("+{}", line));
2827 }
2828 }
2829 }
2830 }
2831
2832 hunks.push(Hunk {
2833 lines,
2834 old_start_line: old_start,
2835 new_start_line: new_start,
2836 });
2837 }
2838
2839 Ok(Patch {
2840 file_path: path,
2841 hunks,
2842 ends_with_newline: new_text.ends_with('\n') || new_text.is_empty(),
2843 })
2844 }
2845
2846 /// Creates a new `Patch` that reverses the changes in this one.
2847 ///
2848 /// Each hunk in the patch is inverted, swapping additions and deletions.
2849 /// This is useful for "un-applying" a patch.
2850 ///
2851 /// **Note:** The `ends_with_newline` status of the reversed patch is ambiguous
2852 /// in the unified diff format, so it defaults to `true`.
2853 ///
2854 /// # Returns
2855 ///
2856 /// A new [`Patch`] object with all hunks inverted.
2857 ///
2858 /// # Examples
2859 ///
2860 /// ```
2861 /// # use mpatch::{Patch, Hunk};
2862 /// let patch = Patch {
2863 /// file_path: "file.txt".into(),
2864 /// hunks: vec![Hunk {
2865 /// lines: vec![
2866 /// " context".to_string(),
2867 /// "-deleted".to_string(),
2868 /// "+added".to_string(),
2869 /// ],
2870 /// old_start_line: Some(10),
2871 /// new_start_line: Some(10),
2872 /// }],
2873 /// ends_with_newline: true,
2874 /// };
2875 ///
2876 /// let inverted = patch.invert();
2877 /// let inverted_hunk = &inverted.hunks[0];
2878 ///
2879 /// assert_eq!(inverted_hunk.removed_lines(), vec!["added"]);
2880 /// assert_eq!(inverted_hunk.added_lines(), vec!["deleted"]);
2881 /// ```
2882 pub fn invert(&self) -> Patch {
2883 Patch {
2884 file_path: self.file_path.clone(),
2885 hunks: self.hunks.iter().map(|h| h.invert()).collect(),
2886 // Inverting this is non-trivial. A standard diff doesn't record
2887 // the newline status of the original file if the new file has one.
2888 // We'll assume the inverted patch will result in a file with a newline.
2889 ends_with_newline: true,
2890 }
2891 }
2892
2893 /// Checks if the patch represents a file creation.
2894 ///
2895 /// A patch is considered a creation if its first hunk is an addition-only
2896 /// hunk that applies to an empty file (i.e., its "match block" is empty).
2897 ///
2898 /// # Returns
2899 ///
2900 /// `true` if the patch represents a file creation, `false` otherwise.
2901 ///
2902 /// # Examples
2903 ///
2904 /// ````
2905 /// # use mpatch::parse_single_patch;
2906 /// let creation_diff = r#"
2907 /// ```diff
2908 /// --- a/new_file.txt
2909 /// +++ b/new_file.txt
2910 /// @@ -0,0 +1,2 @@
2911 /// +Hello
2912 /// +World
2913 /// ```
2914 /// "#;
2915 /// let patch = parse_single_patch(creation_diff).unwrap();
2916 /// assert!(patch.is_creation());
2917 /// ````
2918 pub fn is_creation(&self) -> bool {
2919 self.hunks
2920 .first()
2921 .is_some_and(|h| h.old_start_line == Some(0) || h.get_match_block().is_empty())
2922 }
2923
2924 /// Checks if the patch represents a full file deletion.
2925 ///
2926 /// A patch is considered a deletion if it contains at least one hunk, and
2927 /// all of its hunks result in removing content without adding any new content
2928 /// (i.e., their "replace blocks" are empty). This is typical for a diff
2929 /// that empties a file.
2930 ///
2931 /// # Returns
2932 ///
2933 /// `true` if the patch represents a file deletion, `false` otherwise.
2934 ///
2935 /// # Examples
2936 ///
2937 /// ````
2938 /// # use mpatch::parse_single_patch;
2939 /// let deletion_diff = r#"
2940 /// ```diff
2941 /// --- a/old_file.txt
2942 /// +++ b/old_file.txt
2943 /// @@ -1,2 +0,0 @@
2944 /// -Hello
2945 /// -World
2946 /// ```
2947 /// "#;
2948 /// let patch = parse_single_patch(deletion_diff).unwrap();
2949 /// assert!(patch.is_deletion());
2950 /// ````
2951 pub fn is_deletion(&self) -> bool {
2952 !self.hunks.is_empty()
2953 && self
2954 .hunks
2955 .iter()
2956 .all(|h| h.new_start_line == Some(0) || h.get_replace_block().is_empty())
2957 }
2958}
2959
2960impl std::fmt::Display for Patch {
2961 /// Formats the patch into a valid unified diff string for a single file.
2962 ///
2963 /// This provides a canonical string representation of the entire patch,
2964 /// including the `---` and `+++` file headers, followed by the
2965 /// formatted content of all its hunks. It also correctly handles the
2966 /// `\ No newline at end of file` marker when necessary.
2967 ///
2968 /// This is useful for logging, debugging, or serializing a `Patch` object
2969 /// back to its original text format.
2970 ///
2971 /// # Arguments
2972 ///
2973 /// * `f` - The formatter to write the output to.
2974 ///
2975 /// # Returns
2976 ///
2977 /// `Ok(())` if the formatting was successful.
2978 ///
2979 /// # Errors
2980 ///
2981 /// Returns `Err(std::fmt::Error)` if writing to the formatter fails.
2982 ///
2983 /// # Examples
2984 ///
2985 /// ```
2986 /// # use mpatch::{Patch, Hunk};
2987 /// let patch = Patch {
2988 /// file_path: "src/main.rs".into(),
2989 /// hunks: vec![Hunk {
2990 /// lines: vec![
2991 /// "-old".to_string(),
2992 /// "+new".to_string(),
2993 /// ],
2994 /// old_start_line: Some(1),
2995 /// new_start_line: Some(1),
2996 /// }],
2997 /// ends_with_newline: false, // To test the marker
2998 /// };
2999 ///
3000 /// let expected_output = concat!(
3001 /// "--- a/src/main.rs\n",
3002 /// "+++ b/src/main.rs\n",
3003 /// "@@ -1,1 +1,1 @@\n",
3004 /// "-old\n",
3005 /// "+new\n",
3006 /// "\\ No newline at end of file"
3007 /// );
3008 ///
3009 /// assert_eq!(patch.to_string(), expected_output);
3010 /// ```
3011 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3012 writeln!(f, "--- a/{}", self.file_path.display())?;
3013 writeln!(f, "+++ b/{}", self.file_path.display())?;
3014
3015 for hunk in &self.hunks {
3016 write!(f, "{}", hunk)?;
3017 }
3018
3019 if !self.ends_with_newline && !self.hunks.is_empty() {
3020 write!(f, "\\ No newline at end of file")?;
3021 }
3022
3023 Ok(())
3024 }
3025}
3026
3027// --- Core Logic ---
3028
3029/// Identifies the syntactic format of a patch content string.
3030///
3031/// This enum is returned by [`detect_patch()`] and used internally by
3032/// [`parse_auto()`] to determine which parsing strategy to apply.
3033///
3034/// It distinguishes between raw diffs (commonly output by `git diff`), diffs wrapped
3035/// in Markdown code blocks (commonly output by LLMs), and conflict marker blocks
3036/// (used in merge conflicts or specific AI suggestions).
3037///
3038/// # Examples
3039///
3040/// ```
3041/// use mpatch::{detect_patch, PatchFormat};
3042///
3043/// let content = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-a\n+b";
3044/// assert_eq!(detect_patch(content), PatchFormat::Unified);
3045/// ```
3046#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3047#[non_exhaustive]
3048pub enum PatchFormat {
3049 /// A standard Unified Diff format.
3050 ///
3051 /// This format is characterized by file headers starting with `---` and `+++`,
3052 /// or hunk headers starting with `@@`.
3053 ///
3054 /// # Examples
3055 /// ```text
3056 /// --- a/file.rs
3057 /// +++ b/file.rs
3058 /// @@ -1,3 +1,3 @@
3059 /// fn main() {
3060 /// - println!("Old");
3061 /// + println!("New");
3062 /// }
3063 /// ```
3064 Unified,
3065
3066 /// A Markdown file containing diff code blocks.
3067 ///
3068 /// This format is characterized by the presence of code fences (e.g., ` ```diff `)
3069 /// containing patch data. `mpatch` will extract and parse the content inside these blocks.
3070 ///
3071 /// # Examples
3072 /// ````text
3073 /// Here is the fix for your issue:
3074 ///
3075 /// ```diff
3076 /// --- a/src/main.rs
3077 /// +++ b/src/main.rs
3078 /// @@ -1 +1 @@
3079 /// -old_function();
3080 /// +new_function();
3081 /// ```
3082 /// ````
3083 Markdown,
3084
3085 /// A file containing Conflict Markers.
3086 ///
3087 /// This format is characterized by the specific markers `<<<<`, `====`, and `>>>>`.
3088 /// It is commonly found in Git merge conflicts or AI code suggestions that use
3089 /// this format to denote "before" and "after" states without full diff headers.
3090 ///
3091 /// # Examples
3092 /// ```text
3093 /// fn calculate() {
3094 /// <<<<
3095 /// return x + y;
3096 /// ====
3097 /// return x * y;
3098 /// >>>>
3099 /// }
3100 /// ```
3101 Conflict,
3102
3103 /// The format could not be determined.
3104 ///
3105 /// The content did not contain any recognizable signatures (such as diff headers,
3106 /// markdown fences, or conflict markers).
3107 ///
3108 /// # Examples
3109 ///
3110 /// ```
3111 /// use mpatch::{detect_patch, PatchFormat};
3112 /// let content = "Just some random text.";
3113 /// assert_eq!(detect_patch(content), PatchFormat::Unknown);
3114 /// ```
3115 Unknown,
3116}
3117
3118/// Automatically detects the patch format of the provided content.
3119///
3120/// This function scans the content efficiently (without parsing the full structure)
3121/// to determine if it contains Markdown code blocks, standard unified diff headers,
3122/// or conflict markers.
3123///
3124/// ## Behavior
3125///
3126/// The detection follows this priority:
3127/// 1. **Markdown**: If code fences (3+ backticks) are found containing diff signatures, it is treated as Markdown.
3128/// 2. **Unified**: If `--- a/` or `diff --git` headers are found, it is treated as a Unified Diff.
3129/// 3. **Conflict**: If `<<<<` markers are found, it is treated as Conflict Markers.
3130///
3131/// # Arguments
3132///
3133/// * `content` - A string slice containing the patch data to analyze.
3134///
3135/// # Returns
3136///
3137/// The detected [`PatchFormat`].
3138///
3139/// # Examples
3140/// ```rust
3141/// use mpatch::{detect_patch, PatchFormat};
3142///
3143/// let md = "```diff\n--- a/f\n+++ b/f\n```";
3144/// assert_eq!(detect_patch(md), PatchFormat::Markdown);
3145///
3146/// let raw = "--- a/f\n+++ b/f\n@@ -1 +1 @@";
3147/// assert_eq!(detect_patch(raw), PatchFormat::Unified);
3148/// ```
3149pub fn detect_patch(content: &str) -> PatchFormat {
3150 let mut lines = content.lines().peekable();
3151 let mut in_code_block = false;
3152 let mut current_fence_len = 0;
3153 let mut has_unified_headers = false;
3154 let mut has_conflict_start = false;
3155 let mut has_conflict_middle_or_end = false;
3156 let mut has_conflict_markers = false;
3157
3158 while let Some(line) = lines.next() {
3159 // Check for Markdown code blocks
3160 let trimmed = line.trim_start();
3161 if trimmed.starts_with("```") {
3162 let fence_len = trimmed.chars().take_while(|&c| c == '`').count();
3163 if fence_len >= 3 {
3164 if !in_code_block {
3165 in_code_block = true;
3166 current_fence_len = fence_len;
3167 let info = &trimmed[fence_len..];
3168 if info.contains("diff") || info.contains("patch") {
3169 return PatchFormat::Markdown;
3170 }
3171 } else if fence_len >= current_fence_len {
3172 in_code_block = false;
3173 current_fence_len = 0;
3174 }
3175 continue;
3176 }
3177 }
3178
3179 // Check for Unified Diff headers
3180 let is_diff_git = line.starts_with("diff --git");
3181 let is_unified_header =
3182 line.starts_with("--- ") && lines.peek().is_some_and(|l| l.starts_with("+++ "));
3183 let is_hunk_header = line.starts_with("@@ -") && line.contains(" @@");
3184
3185 if is_diff_git || is_unified_header || is_hunk_header {
3186 if in_code_block {
3187 return PatchFormat::Markdown;
3188 }
3189 has_unified_headers = true;
3190 }
3191
3192 // Check for Conflict Markers
3193 let trimmed = line.trim_start();
3194 if trimmed.starts_with("<<<<") {
3195 has_conflict_start = true;
3196 } else if (trimmed.starts_with("====") || trimmed.starts_with(">>>>")) && has_conflict_start
3197 {
3198 has_conflict_middle_or_end = true;
3199 }
3200
3201 if has_conflict_start && has_conflict_middle_or_end {
3202 if in_code_block {
3203 return PatchFormat::Markdown;
3204 }
3205 has_conflict_markers = true;
3206 }
3207 }
3208
3209 if has_unified_headers {
3210 PatchFormat::Unified
3211 } else if has_conflict_markers {
3212 PatchFormat::Conflict
3213 } else {
3214 PatchFormat::Unknown
3215 }
3216}
3217
3218/// Automatically detects the format of the input text and parses it into a list of patches.
3219///
3220/// This is the recommended entry point for most use cases, as it robustly handles
3221/// the various ways diffs are commonly presented (e.g., inside Markdown code blocks
3222/// from LLMs, as raw output from `git diff`, or as conflict markers in source files).
3223///
3224/// ## Supported Formats
3225///
3226/// 1. **Markdown:** Code blocks fenced with backticks (e.g., ` ```diff `) containing
3227/// diff content. This is the standard output format for AI coding assistants.
3228/// 2. **Unified Diff:** Standard diffs containing `--- a/path` and `+++ b/path` headers.
3229/// 3. **Conflict Markers:** Blocks delimited by `<<<<`, `====`, and `>>>>`. These are
3230/// parsed into patches where the "old" content is removed and the "new" content is added.
3231///
3232/// ## Behavior
3233///
3234/// The function first attempts to detect the format using lightweight heuristics
3235/// (see [`detect_patch`]).
3236///
3237/// - If **Markdown** is detected, it extracts patches from all valid code blocks.
3238/// - If **Unified Diff** headers are detected, it parses the entire string as a raw diff.
3239/// - If **Conflict Markers** are detected, it parses the blocks into patches targeting a generic file path.
3240/// - If the format is **Unknown**, it attempts to parse the content as a raw diff
3241/// as a fallback. This allows parsing fragments that might lack full file headers
3242/// but contain valid hunks.
3243///
3244/// # Arguments
3245///
3246/// * `content` - A string slice containing the patch data to analyze.
3247///
3248/// # Returns
3249///
3250/// A `Result` containing a vector of [`Patch`] objects on success.
3251///
3252/// # Errors
3253///
3254/// Returns `Err(`[`ParseError`]`)` if the content is detected as a format but fails to parse
3255/// (e.g., a unified diff missing file headers).
3256///
3257/// # Examples
3258///
3259/// **Parsing a Markdown string:**
3260/// ````
3261/// use mpatch::parse_auto;
3262///
3263/// let md = r#"
3264/// Here is the fix:
3265/// ```diff
3266/// --- a/src/main.rs
3267/// +++ b/src/main.rs
3268/// @@ -1 +1 @@
3269/// -println!("Old");
3270/// +println!("New");
3271/// ```
3272/// "#;
3273///
3274/// let patches = parse_auto(md).unwrap();
3275/// assert_eq!(patches.len(), 1);
3276/// assert_eq!(patches[0].file_path.to_str(), Some("src/main.rs"));
3277/// ````
3278///
3279/// **Parsing a Raw Diff:**
3280/// ````
3281/// use mpatch::parse_auto;
3282///
3283/// let raw = r#"
3284/// --- a/config.toml
3285/// +++ b/config.toml
3286/// @@ -1 +1 @@
3287/// -debug = false
3288/// +debug = true
3289/// "#;
3290///
3291/// let patches = parse_auto(raw).unwrap();
3292/// assert_eq!(patches.len(), 1);
3293/// ````
3294///
3295/// **Parsing Conflict Markers:**
3296/// ````
3297/// use mpatch::parse_auto;
3298///
3299/// let conflict = r#"
3300/// <<<<
3301/// old_code();
3302/// ====
3303/// new_code();
3304/// >>>>
3305/// "#;
3306///
3307/// let patches = parse_auto(conflict).unwrap();
3308/// // Conflict markers don't specify a file, so they get a generic path.
3309/// assert_eq!(patches[0].file_path.to_str(), Some("patch_target"));
3310/// ````
3311pub fn parse_auto(content: &str) -> Result<Vec<Patch>, ParseError> {
3312 let format = detect_patch(content);
3313 debug!("Auto-detected patch format: {:?}", format);
3314 match format {
3315 PatchFormat::Markdown => parse_diffs(content),
3316 PatchFormat::Unified => parse_patches(content),
3317 PatchFormat::Conflict => {
3318 let patches = parse_conflict_markers(content);
3319 debug!("Parsed {} patches from conflict markers.", patches.len());
3320 Ok(patches)
3321 }
3322 PatchFormat::Unknown => {
3323 // If unknown, we try parsing as raw patches as a fallback,
3324 // as it might be a fragment without headers.
3325 debug!("Patch format unknown. Falling back to raw unified diff parsing.");
3326 let patches = parse_patches(content)?;
3327 if !patches.is_empty() {
3328 debug!(
3329 "Fallback parsing successful, found {} patch(es).",
3330 patches.len()
3331 );
3332 Ok(patches)
3333 } else {
3334 // If that yields nothing, return empty.
3335 debug!("Fallback parsing found no patches.");
3336 Ok(Vec::new())
3337 }
3338 }
3339 }
3340}
3341
3342/// Parses a string containing one or more markdown diff blocks into a vector of [`Patch`] objects.
3343///
3344/// This function scans the input `content` for markdown-style code blocks. It supports
3345/// variable-length code fences (e.g., ` ``` ` or ` ```` `) and correctly handles nested
3346/// code blocks.
3347///
3348/// It checks every block to see if it contains valid diff content (Unified Diff or Conflict Markers)
3349/// at the top level of the block. Diffs inside nested code blocks (e.g., examples within documentation)
3350/// are ignored. Blocks that do not contain recognizable patch signatures are skipped efficiently.
3351///
3352/// It supports two formats within the blocks:
3353/// 1. **Unified Diff:** Standard `--- a/file`, `+++ b/file`, `@@ ... @@` format.
3354/// 2. **Conflict Markers:** `<<<<`, `====`, `>>>>` blocks. Since these lack file headers,
3355/// patches will be assigned a generic file path (`patch_target`).
3356///
3357/// For automatic format detection (supporting raw diffs and conflict markers outside of markdown),
3358/// use [`parse_auto()`].
3359///
3360/// # Arguments
3361///
3362/// * `content` - A string slice containing the markdown content to parse.
3363///
3364/// # Returns
3365///
3366/// A `Result` containing a vector of [`Patch`] objects on success.
3367///
3368/// # Errors
3369///
3370/// Returns `Err(`[`ParseError`]`)` if a block looks like a patch (e.g. has `--- a/file`) but fails
3371/// to parse correctly. Blocks that simply lack headers are ignored.
3372///
3373/// # Examples
3374///
3375/// ````rust
3376/// use mpatch::parse_diffs;
3377///
3378/// let diff_content = r#"
3379/// ```rust
3380/// // This block will be checked, and if it contains a diff, it will be parsed.
3381/// --- a/src/main.rs
3382/// +++ b/src/main.rs
3383/// @@ -1,3 +1,3 @@
3384/// fn main() {
3385/// - println!("Hello, world!");
3386/// + println!("Hello, mpatch!");
3387/// }
3388/// ```
3389/// "#;
3390///
3391/// let patches = parse_diffs(diff_content).unwrap();
3392/// assert_eq!(patches.len(), 1);
3393/// assert_eq!(patches[0].file_path.to_str(), Some("src/main.rs"));
3394/// assert_eq!(patches[0].hunks.len(), 1);
3395/// ````
3396pub fn parse_diffs(content: &str) -> Result<Vec<Patch>, ParseError> {
3397 debug!("Starting to parse diffs from content (Markdown mode).");
3398 let mut all_patches = Vec::new();
3399 let mut lines = content.lines().enumerate().peekable();
3400
3401 // The `find` call consumes the iterator until it finds the start of a diff block.
3402 // The loop continues searching for more blocks from where the last one ended.
3403 while let Some((line_index, line_text)) = lines.by_ref().find(|(_, line)| {
3404 let trimmed = line.trim_start();
3405 trimmed.starts_with("```") && trimmed.chars().take_while(|&c| c == '`').count() >= 3
3406 }) {
3407 let trimmed = line_text.trim_start();
3408 let fence_len = trimmed.chars().take_while(|&c| c == '`').count();
3409 let opening_indent = line_text.len() - trimmed.len();
3410
3411 trace!(
3412 "Found potential diff block start on line {}: '{}'",
3413 line_index,
3414 line_text
3415 );
3416 let diff_block_start_line = line_index + 1;
3417
3418 let mut block_lines = Vec::new();
3419
3420 // Consume lines until end of block
3421 while let Some((_, line)) = lines.peek() {
3422 let inner_trimmed = line.trim_start();
3423 let current_indent = line.len() - inner_trimmed.len();
3424 if inner_trimmed.starts_with("```")
3425 && inner_trimmed.chars().take_while(|&c| c == '`').count() >= fence_len
3426 && current_indent <= opening_indent
3427 {
3428 lines.next(); // Consume the closing fence
3429 break;
3430 }
3431 let (_, line) = lines.next().unwrap();
3432 block_lines.push(line);
3433 }
3434
3435 if has_patch_signature_at_level_1(&block_lines) {
3436 debug!(
3437 "Parsing diff block starting on line {}.",
3438 diff_block_start_line
3439 );
3440 let block_patches = parse_generic_block_lines(block_lines, diff_block_start_line)?;
3441 all_patches.extend(block_patches);
3442 } else {
3443 trace!(
3444 "Skipping code block starting on line {} (no patch markers found).",
3445 diff_block_start_line
3446 );
3447 }
3448 }
3449
3450 debug!(
3451 "Finished parsing. Found {} patch(es) in total.",
3452 all_patches.len()
3453 );
3454 Ok(all_patches)
3455}
3456
3457/// Checks if the provided lines contain a patch signature at the first level of nesting.
3458///
3459/// This ensures that we don't parse diffs that are inside nested code blocks (e.g.,
3460/// a diff example inside a markdown block).
3461fn has_patch_signature_at_level_1<S: AsRef<str>>(lines: &[S]) -> bool {
3462 let mut in_nested_block = false;
3463 let mut current_fence_len = 0;
3464
3465 for line in lines {
3466 let line = line.as_ref();
3467 let trimmed = line.trim_start();
3468
3469 // Check for nested block boundaries
3470 if trimmed.starts_with("```") {
3471 let fence_len = trimmed.chars().take_while(|&c| c == '`').count();
3472 if fence_len >= 3 {
3473 if !in_nested_block {
3474 in_nested_block = true;
3475 current_fence_len = fence_len;
3476 continue;
3477 } else if fence_len >= current_fence_len {
3478 in_nested_block = false;
3479 current_fence_len = 0;
3480 continue;
3481 }
3482 }
3483 }
3484
3485 if !in_nested_block
3486 && (line.starts_with("--- ")
3487 || line.starts_with("diff --git")
3488 || trimmed.starts_with("<<<<")
3489 || trimmed.starts_with("====")
3490 || trimmed.starts_with(">>>>"))
3491 {
3492 return true;
3493 }
3494 }
3495 false
3496}
3497
3498/// Helper function to parse a block of lines that could be Unified or Conflict.
3499/// This consolidates the fallback logic previously inside `parse_diffs`.
3500fn parse_generic_block_lines(
3501 lines: Vec<&str>,
3502 start_line: usize,
3503) -> Result<Vec<Patch>, ParseError> {
3504 trace!(
3505 " Attempting to parse generic block starting at line {} as standard unified diff.",
3506 start_line
3507 );
3508 // 1. Try parsing as standard unified diff
3509 let standard_result = parse_patches_from_lines(lines.clone().into_iter());
3510
3511 match standard_result {
3512 Ok(patches) => {
3513 if !patches.is_empty() {
3514 trace!(" Successfully parsed block as standard unified diff.");
3515 Ok(patches)
3516 } else {
3517 trace!(" Standard parser found no patches. Attempting conflict markers.");
3518 // 2. If standard parsing found nothing, try conflict markers
3519 let conflict_patches = parse_conflict_markers_from_lines(lines.into_iter());
3520 if !conflict_patches.is_empty() {
3521 trace!(" Successfully parsed block as conflict markers.");
3522 } else {
3523 trace!(" No conflict markers found either.");
3524 }
3525 Ok(conflict_patches)
3526 }
3527 }
3528 Err(e) => {
3529 trace!(
3530 " Standard parsing failed ({}). Attempting conflict markers.",
3531 e
3532 );
3533 // 3. If standard parsing failed (e.g. missing header), check for conflict markers
3534 let conflict_patches = parse_conflict_markers_from_lines(lines.into_iter());
3535 if !conflict_patches.is_empty() {
3536 trace!(" Successfully parsed block as conflict markers.");
3537 Ok(conflict_patches)
3538 } else {
3539 trace!(" Conflict marker parsing also failed. Returning original error.");
3540 // 4. Return original error if both failed
3541 match e {
3542 ParseError::MissingFileHeader { .. } => {
3543 Err(ParseError::MissingFileHeader { line: start_line })
3544 }
3545 }
3546 }
3547 }
3548 }
3549}
3550
3551/// Parses a string containing a diff and returns a single [`Patch`] object.
3552///
3553/// This is a convenience function that wraps [`parse_auto()`] but enforces that the
3554/// input `content` results in exactly one `Patch`. It is useful when you expect
3555/// a diff for a single file and want to handle the "zero or many" cases as an error.
3556///
3557/// # Arguments
3558///
3559/// * `content` - A string slice containing the text to parse. This can be a raw
3560/// Unified Diff, a Markdown block, or a set of Conflict Markers.
3561///
3562/// # Returns
3563///
3564/// A single [`Patch`] object on success.
3565///
3566/// # Errors
3567///
3568/// Returns a [`SingleParseError`] if:
3569/// - The underlying parsing fails (e.g., a diff block is missing a file header).
3570/// - No patches are found in the content.
3571/// - More than one patch is found in the content.
3572///
3573/// # Examples
3574///
3575/// ````rust
3576/// # use mpatch::{parse_single_patch, SingleParseError};
3577/// // --- Success Case ---
3578/// let diff_content = r#"
3579/// ```diff
3580/// --- a/src/main.rs
3581/// +++ b/src/main.rs
3582/// @@ -1,3 +1,3 @@
3583/// fn main() {
3584/// - println!("Hello, world!");
3585/// + println!("Hello, mpatch!");
3586/// }
3587/// ```
3588/// "#;
3589/// let patch = parse_single_patch(diff_content).unwrap();
3590/// assert_eq!(patch.file_path.to_str(), Some("src/main.rs"));
3591///
3592/// // --- Error Case (Multiple Patches) ---
3593/// let multi_file_diff = r#"
3594/// ```diff
3595/// --- a/file1.txt
3596/// +++ b/file1.txt
3597/// @@ -1 +1 @@
3598/// -a
3599/// +b
3600/// --- a/file2.txt
3601/// +++ b/file2.txt
3602/// @@ -1 +1 @@
3603/// -c
3604/// +d
3605/// ```
3606/// "#;
3607/// let result = parse_single_patch(multi_file_diff);
3608/// assert!(matches!(result, Err(SingleParseError::MultiplePatchesFound(2))));
3609/// ````
3610pub fn parse_single_patch(content: &str) -> Result<Patch, SingleParseError> {
3611 let mut patches = parse_auto(content)?;
3612
3613 if patches.len() > 1 {
3614 Err(SingleParseError::MultiplePatchesFound(patches.len()))
3615 } else if patches.is_empty() {
3616 Err(SingleParseError::NoPatchesFound)
3617 } else {
3618 // .remove(0) is safe here because we've confirmed the length is 1.
3619 Ok(patches.remove(0))
3620 }
3621}
3622/// Parses a string containing raw unified diff content into a vector of [`Patch`] objects.
3623///
3624/// Unlike [`parse_diffs()`], this function does not look for markdown code blocks.
3625/// It assumes the entire input string is valid unified diff content. This is useful
3626/// when you have a raw `.diff` or `.patch` file, or the output of a `git diff` command.
3627///
3628/// For automatic format detection, use [`parse_auto()`].
3629///
3630/// # Arguments
3631///
3632/// * `content` - A string slice containing the raw unified diff content.
3633///
3634/// # Returns
3635///
3636/// A `Result` containing a vector of [`Patch`] objects on success.
3637///
3638/// # Errors
3639///
3640/// Returns `Err(`[`ParseError::MissingFileHeader`]`)` if the content contains patch
3641/// hunks but no `--- a/path/to/file` header.
3642///
3643/// # Examples
3644///
3645/// ```rust
3646/// use mpatch::parse_patches;
3647///
3648/// let raw_diff = r#"
3649/// --- a/src/main.rs
3650/// +++ b/src/main.rs
3651/// @@ -1,3 +1,3 @@
3652/// fn main() {
3653/// - println!("Hello, world!");
3654/// + println!("Hello, mpatch!");
3655/// }
3656/// "#;
3657///
3658/// let patches = parse_patches(raw_diff).unwrap();
3659/// assert_eq!(patches.len(), 1);
3660/// assert_eq!(patches[0].file_path.to_str(), Some("src/main.rs"));
3661/// ```
3662pub fn parse_patches(content: &str) -> Result<Vec<Patch>, ParseError> {
3663 debug!("Starting to parse raw diff content.");
3664 parse_patches_from_lines(content.lines())
3665}
3666
3667/// Parses a string containing "Conflict Marker" style diffs (<<<<, ====, >>>>).
3668///
3669/// This format is common in Git merge conflicts or AI-generated code suggestions.
3670/// Since this format typically lacks file headers, the resulting [`Patch`] objects
3671/// will have a generic file path (`patch_target`).
3672///
3673/// **Warning:** Because this format lacks target file information, it is
3674/// generally unsuitable for batch-applying patches to a directory unless
3675/// the target file is manually specified or renamed.
3676///
3677/// This function treats text outside the markers as context lines, text between
3678/// `<<<<` and `====` as deletions, and text between `====` and `>>>>` as additions.
3679///
3680/// For automatic format detection, use [`parse_auto()`].
3681///
3682/// # Arguments
3683///
3684/// * `content` - A string slice containing the conflict marker content.
3685///
3686/// # Returns
3687///
3688/// A vector of [`Patch`] objects parsed from the conflict markers.
3689///
3690/// # Examples
3691///
3692/// ```rust
3693/// use mpatch::parse_conflict_markers;
3694///
3695/// let content = r#"
3696/// fn main() {
3697/// <<<<
3698/// println!("Old");
3699/// ====
3700/// println!("New");
3701/// >>>>
3702/// }
3703/// "#;
3704///
3705/// let patches = parse_conflict_markers(content);
3706/// assert_eq!(patches.len(), 1);
3707/// assert_eq!(patches[0].hunks[0].removed_lines(), vec![" println!(\"Old\");"]);
3708/// assert_eq!(patches[0].hunks[0].added_lines(), vec![" println!(\"New\");"]);
3709/// ```
3710pub fn parse_conflict_markers(content: &str) -> Vec<Patch> {
3711 debug!("Starting to parse conflict marker content.");
3712 let patches = parse_conflict_markers_from_lines(content.lines());
3713 debug!("Finished parsing. Found {} patch(es).", patches.len());
3714 patches
3715}
3716
3717/// Parses an iterator of lines containing raw unified diff content into a vector of [`Patch`] objects.
3718///
3719/// This is a lower-level, more flexible alternative to [`parse_patches()`]. It is useful
3720/// when you already have the diff content as a sequence of lines (e.g., from reading a
3721/// file line-by-line) and want to avoid allocating the entire content as a single string.
3722///
3723/// It assumes the entire sequence of lines is valid unified diff content and does not
3724/// look for markdown code blocks.
3725///
3726/// # Arguments
3727///
3728/// * `lines` - An iterator that yields string slices, where each slice is a line of the diff.
3729///
3730/// # Returns
3731///
3732/// A `Result` containing a vector of [`Patch`] objects on success.
3733///
3734/// # Errors
3735///
3736/// Returns `Err(`[`ParseError::MissingFileHeader`]`)` if the content contains patch
3737/// hunks but no `--- a/path/to/file` header. The `line` number in the error will
3738/// correspond to the first hunk header (e.g., `@@ ... @@`) found.
3739///
3740/// # Examples
3741///
3742/// ```rust
3743/// use mpatch::parse_patches_from_lines;
3744///
3745/// let raw_diff_lines = vec![
3746/// "--- a/src/main.rs",
3747/// "+++ b/src/main.rs",
3748/// "@@ -1,3 +1,3 @@",
3749/// " fn main() {",
3750/// "- println!(\"Hello, world!\");",
3751/// "+ println!(\"Hello, mpatch!\");",
3752/// " }",
3753/// ];
3754///
3755/// let patches = parse_patches_from_lines(raw_diff_lines.into_iter()).unwrap();
3756/// assert_eq!(patches.len(), 1);
3757/// assert_eq!(patches[0].file_path.to_str(), Some("src/main.rs"));
3758/// ```
3759pub fn parse_patches_from_lines<'a, I>(lines: I) -> Result<Vec<Patch>, ParseError>
3760where
3761 I: Iterator<Item = &'a str>,
3762{
3763 let mut unmerged_patches: Vec<Patch> = Vec::new();
3764 const HUNK_BUFFER_CAPACITY: usize = 32;
3765
3766 // State variables for the parser as it moves through the diff block.
3767 let mut first_hunk_header_line: Option<usize> = None;
3768 let mut current_file: Option<PathBuf> = None;
3769 let mut current_hunks: Vec<Hunk> = Vec::new();
3770 let mut current_hunk_lines: Vec<String> = Vec::with_capacity(HUNK_BUFFER_CAPACITY);
3771 let mut current_hunk_old_start_line: Option<usize> = None;
3772 let mut current_hunk_new_start_line: Option<usize> = None;
3773 let mut ends_with_newline_for_section = true;
3774
3775 macro_rules! finalize_hunk {
3776 () => {
3777 if current_hunk_old_start_line.is_some() {
3778 trace!(
3779 " Finalizing previous hunk with {} lines.",
3780 current_hunk_lines.len()
3781 );
3782 // Strip trailing empty context lines (often artifacts of spacing between diffs)
3783 while let Some(last) = current_hunk_lines.last() {
3784 if last.trim().is_empty() {
3785 current_hunk_lines.pop();
3786 } else {
3787 break;
3788 }
3789 }
3790 current_hunks.push(Hunk {
3791 lines: std::mem::replace(
3792 &mut current_hunk_lines,
3793 Vec::with_capacity(HUNK_BUFFER_CAPACITY),
3794 ),
3795 old_start_line: current_hunk_old_start_line,
3796 new_start_line: current_hunk_new_start_line,
3797 });
3798 }
3799 };
3800 }
3801
3802 for (line_idx, line) in lines.enumerate() {
3803 if let Some(stripped_line) = line.strip_prefix("--- ") {
3804 trace!(" Found file header line: '{}'", line);
3805 // A `---` line always signals a new file section.
3806 // Finalize the previous file's patch section if it exists.
3807 if let Some(existing_file) = ¤t_file {
3808 finalize_hunk!();
3809 if !current_hunks.is_empty() {
3810 debug!(
3811 " Finalizing patch section for '{}' with {} hunk(s).",
3812 existing_file.display(),
3813 current_hunks.len()
3814 );
3815 unmerged_patches.push(Patch {
3816 file_path: existing_file.clone(),
3817 hunks: std::mem::take(&mut current_hunks),
3818 ends_with_newline: ends_with_newline_for_section,
3819 });
3820 }
3821 }
3822
3823 // Reset for the new file section.
3824 trace!(" Resetting parser state for new file section.");
3825 current_file = None;
3826 current_hunk_lines.clear();
3827 current_hunk_old_start_line = None;
3828 current_hunk_new_start_line = None;
3829 ends_with_newline_for_section = true;
3830
3831 let path_part = stripped_line.trim();
3832 if path_part == "/dev/null" || path_part == "a/dev/null" {
3833 trace!(" Path is /dev/null, indicating file creation.");
3834 // File creation, path will be in `+++` line.
3835 } else {
3836 let path_str = path_part.strip_prefix("a/").unwrap_or(path_part);
3837 debug!(" Starting new patch section for file: '{}'", path_str);
3838 current_file = Some(PathBuf::from(path_str.trim()));
3839 }
3840 } else if let Some(stripped_line) = line.strip_prefix("+++ ") {
3841 trace!(" Found '+++' line: '{}'", line);
3842 if current_file.is_none() {
3843 let path_part = stripped_line.trim();
3844 let path_str = path_part.strip_prefix("b/").unwrap_or(path_part);
3845 debug!(" Set file path from '+++' line: '{}'", path_str);
3846 current_file = Some(PathBuf::from(path_str.trim()));
3847 }
3848 } else if line.starts_with("@@") {
3849 trace!(" Found hunk header: '{}'", line);
3850 finalize_hunk!();
3851 if first_hunk_header_line.is_none() {
3852 first_hunk_header_line = Some(line_idx + 1);
3853 }
3854 let (old, new) = parse_hunk_header(line);
3855 trace!(" Parsed old_start={:?}, new_start={:?}", old, new);
3856 current_hunk_old_start_line = old;
3857 current_hunk_new_start_line = new;
3858 } else if line.starts_with(['+', '-', ' ']) {
3859 // Only treat this as a hunk line if we're actually inside a hunk.
3860 if current_hunk_old_start_line.is_some() {
3861 current_hunk_lines.push(line.to_string());
3862 }
3863 } else if line.starts_with('\\') {
3864 // This line only makes sense inside a hunk.
3865 if current_hunk_old_start_line.is_some() {
3866 trace!(" Found '\\ No newline at end of file' marker.");
3867 if let Some(last_line) = current_hunk_lines.last() {
3868 if last_line.starts_with('+') || last_line.starts_with(' ') {
3869 ends_with_newline_for_section = false;
3870 }
3871 }
3872 }
3873 } else if is_git_header_line(line) {
3874 trace!(" Ignoring Git header line: '{}'", line.trim_end());
3875 } else if current_hunk_old_start_line.is_some() {
3876 trace!(
3877 " Adding unrecognized line as context to current hunk: '{}'",
3878 line.trim_end()
3879 );
3880 current_hunk_lines.push(format!(" {}", line));
3881 }
3882 }
3883
3884 // Finalize the last hunk and patch section after the loop.
3885 debug!(" End of diff block. Finalizing last hunk and patch section.");
3886 finalize_hunk!();
3887
3888 if let Some(file_path) = current_file {
3889 if !current_hunks.is_empty() {
3890 debug!(
3891 " Finalizing patch section for '{}' with {} hunk(s).",
3892 file_path.display(),
3893 current_hunks.len()
3894 );
3895 unmerged_patches.push(Patch {
3896 file_path,
3897 hunks: current_hunks,
3898 ends_with_newline: ends_with_newline_for_section,
3899 });
3900 }
3901 } else if !current_hunks.is_empty() {
3902 let error_line = first_hunk_header_line.unwrap_or(1);
3903 warn!(
3904 "Found hunks starting near line {} but no file path header ('--- a/path').",
3905 error_line
3906 );
3907 return Err(ParseError::MissingFileHeader { line: error_line });
3908 }
3909
3910 // Merge patch sections for the same file.
3911 if unmerged_patches.is_empty() {
3912 return Ok(vec![]);
3913 }
3914
3915 debug!(
3916 "Merging {} patch section(s) found in the block.",
3917 unmerged_patches.len()
3918 );
3919 let mut merged_patches: Vec<Patch> = Vec::new();
3920 for patch_section in unmerged_patches {
3921 if let Some(existing_patch) = merged_patches
3922 .iter_mut()
3923 .find(|p| p.file_path == patch_section.file_path)
3924 {
3925 debug!(
3926 " Merging {} hunk(s) for '{}' into existing patch.",
3927 patch_section.hunks.len(),
3928 patch_section.file_path.display()
3929 );
3930 existing_patch.hunks.extend(patch_section.hunks);
3931 existing_patch.ends_with_newline = patch_section.ends_with_newline;
3932 } else {
3933 debug!(
3934 " Adding new patch for '{}'.",
3935 patch_section.file_path.display()
3936 );
3937 merged_patches.push(patch_section);
3938 }
3939 }
3940
3941 Ok(merged_patches)
3942}
3943
3944/// Checks if a line is a standard Git diff header that should be ignored when parsing hunks.
3945fn is_git_header_line(line: &str) -> bool {
3946 line.starts_with("diff --git")
3947 || line.starts_with("index ")
3948 || line.starts_with("old mode ")
3949 || line.starts_with("new mode ")
3950 || line.starts_with("new file mode ")
3951 || line.starts_with("deleted file mode ")
3952 || line.starts_with("similarity index ")
3953 || line.starts_with("copy from ")
3954 || line.starts_with("copy to ")
3955 || line.starts_with("rename from ")
3956 || line.starts_with("rename to ")
3957}
3958
3959/// Parses an iterator of lines containing "Conflict Marker" style diffs.
3960///
3961/// See [`parse_conflict_markers`] for details.
3962fn parse_conflict_markers_from_lines<'a, I>(lines: I) -> Vec<Patch>
3963where
3964 I: Iterator<Item = &'a str>,
3965{
3966 let mut hunk_lines = Vec::new();
3967 let mut has_start = false;
3968 let mut has_middle_or_end = false;
3969
3970 enum State {
3971 Context,
3972 Old,
3973 New,
3974 }
3975 let mut state = State::Context;
3976
3977 for line in lines {
3978 if line.trim_start().starts_with("<<<<") {
3979 state = State::Old;
3980 has_start = true;
3981 continue;
3982 } else if line.trim_start().starts_with("====") {
3983 state = State::New;
3984 if has_start {
3985 has_middle_or_end = true;
3986 }
3987 continue;
3988 } else if line.trim_start().starts_with(">>>>") {
3989 state = State::Context;
3990 if has_start {
3991 has_middle_or_end = true;
3992 }
3993 continue;
3994 }
3995
3996 match state {
3997 State::Context => hunk_lines.push(format!(" {}", line)),
3998 State::Old => hunk_lines.push(format!("-{}", line)),
3999 State::New => hunk_lines.push(format!("+{}", line)),
4000 }
4001 }
4002
4003 if !(has_start && has_middle_or_end) {
4004 return Vec::new();
4005 }
4006
4007 // Create a single patch with a single hunk representing the entire block.
4008 // Since we don't have line numbers, we leave them as None.
4009 let hunk = Hunk {
4010 lines: hunk_lines,
4011 old_start_line: None,
4012 new_start_line: None,
4013 };
4014
4015 // Since conflict markers don't specify a file, we use a placeholder.
4016 // The user can override this or use `patch_content_str` where it doesn't matter.
4017 vec![Patch {
4018 file_path: PathBuf::from("patch_target"),
4019 hunks: vec![hunk],
4020 ends_with_newline: true, // Assumption
4021 }]
4022}
4023
4024/// Converts a `std::io::Error` into a more specific `PatchError`.
4025fn map_io_error(path: PathBuf, e: std::io::Error) -> PatchError {
4026 match e.kind() {
4027 std::io::ErrorKind::PermissionDenied => PatchError::PermissionDenied { path },
4028 std::io::ErrorKind::IsADirectory => PatchError::TargetIsDirectory { path },
4029 _ => PatchError::Io { path, source: e },
4030 }
4031}
4032
4033/// Ensures a relative path, when joined to a base directory, resolves to a location
4034/// that is still inside that base directory.
4035///
4036/// This is a critical security function to prevent path traversal attacks (e.g.,
4037/// a malicious patch trying to modify `../../etc/passwd`). It works by canonicalizing
4038/// both the base directory and the final target path to their absolute, symlink-resolved
4039/// forms and then checking if the target path is a child of the base directory.
4040///
4041/// # Arguments
4042///
4043/// * `base_dir` - The trusted root directory.
4044/// * `relative_path` - The untrusted relative path to be validated.
4045///
4046/// # Returns
4047///
4048/// The safe, canonicalized, absolute path of the target if validation succeeds.
4049///
4050/// # Errors
4051///
4052/// - Returns `Err(`[`PatchError::PathTraversal`]`)` if the path resolves outside the `base_dir`.
4053/// - Returns `Err(`[`PatchError::Io`]`)` if an I/O error occurs during path canonicalization (e.g., `base_dir` does not exist).
4054///
4055/// # Examples
4056///
4057/// ```rust
4058/// # use mpatch::{ensure_path_is_safe, PatchError};
4059/// # use std::path::Path;
4060/// # use std::fs;
4061/// # use tempfile::tempdir;
4062/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4063/// let dir = tempdir()?;
4064/// let base_dir = dir.path();
4065///
4066/// // A safe path
4067/// let safe_path = Path::new("src/main.rs");
4068/// let resolved_path = ensure_path_is_safe(base_dir, safe_path)?;
4069/// let canonical_base = fs::canonicalize(base_dir)?;
4070/// assert!(resolved_path.starts_with(&canonical_base));
4071///
4072/// // An unsafe path
4073/// let unsafe_path = Path::new("../secret.txt");
4074/// let result = ensure_path_is_safe(base_dir, unsafe_path);
4075/// assert!(matches!(result, Err(PatchError::PathTraversal(_))));
4076/// # Ok(())
4077/// # }
4078/// ```
4079pub fn ensure_path_is_safe(base_dir: &Path, relative_path: &Path) -> Result<PathBuf, PatchError> {
4080 trace!(
4081 " Checking path safety for base '{}' and relative path '{}'",
4082 base_dir.display(),
4083 relative_path.display()
4084 );
4085 let base_path =
4086 fs::canonicalize(base_dir).map_err(|e| map_io_error(base_dir.to_path_buf(), e))?;
4087
4088 // Lexical check to prevent arbitrary directory creation outside base_dir
4089 let mut virtual_path = base_path.clone();
4090 for component in relative_path.components() {
4091 match component {
4092 std::path::Component::ParentDir => {
4093 if !virtual_path.pop() {
4094 return Err(PatchError::PathTraversal(relative_path.to_path_buf()));
4095 }
4096 if !virtual_path.starts_with(&base_path) {
4097 return Err(PatchError::PathTraversal(relative_path.to_path_buf()));
4098 }
4099 }
4100 std::path::Component::Normal(c) => {
4101 virtual_path.push(c);
4102 // Resolve symlinks for existing components to prevent traversal via symlink.
4103 // If it doesn't exist yet, we just append it lexically (safe because
4104 // non-existent paths cannot be malicious symlinks).
4105 if fs::symlink_metadata(&virtual_path).is_ok() {
4106 virtual_path = fs::canonicalize(&virtual_path)
4107 .map_err(|e| map_io_error(virtual_path.clone(), e))?;
4108 if !virtual_path.starts_with(&base_path) {
4109 return Err(PatchError::PathTraversal(relative_path.to_path_buf()));
4110 }
4111 }
4112 }
4113 std::path::Component::CurDir => {}
4114 std::path::Component::RootDir | std::path::Component::Prefix(_) => {
4115 return Err(PatchError::PathTraversal(relative_path.to_path_buf()));
4116 }
4117 }
4118 }
4119
4120 Ok(virtual_path)
4121}
4122
4123/// A convenience function that applies a slice of [`Patch`] objects to a target directory.
4124///
4125/// This is a high-level convenience function that iterates through a list of
4126/// patches and applies each one to the filesystem using [`apply_patch_to_file()`].
4127/// It aggregates the results, including both successful applications and any
4128/// "hard" errors encountered (like I/O errors). Files resulting in empty content
4129/// will be deleted.
4130///
4131/// This function will continue applying patches even if some fail.
4132///
4133/// # Arguments
4134///
4135/// * `patches` - A slice of [`Patch`] objects to apply.
4136/// * `target_dir` - The base directory where the patches should be applied.
4137/// * `options` - Configuration for the patch operation.
4138///
4139/// # Returns
4140///
4141/// A [`BatchResult`] containing the results of each individual patch operation.
4142///
4143/// # Examples
4144///
4145/// ````
4146/// # use mpatch::{parse_auto, apply_patches_to_dir, ApplyOptions};
4147/// # use std::fs;
4148/// # use tempfile::tempdir;
4149/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4150/// let dir = tempdir()?;
4151/// fs::write(dir.path().join("file1.txt"), "foo\n")?;
4152/// fs::write(dir.path().join("file2.txt"), "baz\n")?;
4153///
4154/// let diff = r#"
4155/// ```diff
4156/// --- a/file1.txt
4157/// +++ b/file1.txt
4158/// @@ -1 +1 @@
4159/// -foo
4160/// +bar
4161/// --- a/file2.txt
4162/// +++ b/file2.txt
4163/// @@ -1 +1 @@
4164/// -baz
4165/// +qux
4166/// ```
4167/// "#;
4168/// let patches = parse_auto(diff)?;
4169/// let options = ApplyOptions::new();
4170///
4171/// let batch_result = apply_patches_to_dir(&patches, dir.path(), options);
4172///
4173/// assert!(batch_result.all_succeeded());
4174/// assert_eq!(fs::read_to_string(dir.path().join("file1.txt"))?, "bar\n");
4175/// assert_eq!(fs::read_to_string(dir.path().join("file2.txt"))?, "qux\n");
4176/// # Ok(())
4177/// # }
4178/// ````
4179pub fn apply_patches_to_dir(
4180 patches: &[Patch],
4181 target_dir: &Path,
4182 options: ApplyOptions,
4183) -> BatchResult {
4184 let results = patches
4185 .iter()
4186 .map(|patch| {
4187 let result = apply_patch_to_file(patch, target_dir, options);
4188 (patch.file_path.clone(), result)
4189 })
4190 .collect();
4191
4192 BatchResult { results }
4193}
4194
4195/// Inverts a list of patches.
4196///
4197/// This is a convenience function that calls [`Patch::invert()`] on every patch
4198/// in the provided slice. It is useful when you want to reverse the effect of
4199/// a multi-file diff (e.g., "un-applying" a set of changes).
4200///
4201/// # Arguments
4202///
4203/// * `patches` - A slice of [`Patch`] objects to invert.
4204///
4205/// # Returns
4206///
4207/// A new vector of [`Patch`] objects with their changes reversed.
4208///
4209/// # Examples
4210///
4211/// ```
4212/// # use mpatch::{parse_auto, invert_patches};
4213/// let diff = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new";
4214/// let patches = parse_auto(diff).unwrap();
4215///
4216/// let reversed = invert_patches(&patches);
4217/// let hunk = &reversed[0].hunks[0];
4218///
4219/// assert_eq!(hunk.removed_lines(), vec!["new"]);
4220/// assert_eq!(hunk.added_lines(), vec!["old"]);
4221/// ```
4222pub fn invert_patches(patches: &[Patch]) -> Vec<Patch> {
4223 patches.iter().map(|p| p.invert()).collect()
4224}
4225
4226/// A convenience function that applies a single [`Patch`] to the filesystem.
4227///
4228/// This function orchestrates the patching process for a single file. It handles
4229/// filesystem interactions like reading the original file and writing the new
4230/// content, while delegating the core patching logic to [`apply_patch_to_content()`].
4231/// If the patch results in empty content, the target file is deleted.
4232///
4233/// # Arguments
4234///
4235/// * `patch` - The [`Patch`] object to apply.
4236/// * `target_dir` - The base directory where the patch should be applied. The
4237/// `patch.file_path` will be joined to this directory.
4238/// * `options` - Configuration for the patch operation, such as `dry_run` and
4239/// `fuzz_factor`.
4240///
4241/// # Returns
4242///
4243/// A [`PatchResult`] on success. The `PatchResult` contains a detailed report
4244/// for each hunk and, if `dry_run` was enabled, a diff of the proposed changes.
4245/// If some hunks failed, the file may be in a partially patched state (unless
4246/// in dry-run mode).
4247///
4248/// # Errors
4249///
4250/// Returns `Err(`[`PatchError`]`)` for "hard" errors like I/O problems, path traversal violations,
4251/// or a missing target file.
4252///
4253/// # Examples
4254///
4255/// ````
4256/// # use mpatch::{parse_single_patch, apply_patch_to_file, ApplyOptions};
4257/// # use std::fs;
4258/// # use tempfile::tempdir;
4259/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4260/// // 1. Setup a temporary directory and a file to patch.
4261/// let dir = tempdir()?;
4262/// let file_path = dir.path().join("hello.txt");
4263/// fs::write(&file_path, "Hello, world!\n")?;
4264///
4265/// // 2. Define and parse the patch.
4266/// let diff_content = r#"
4267/// ```diff
4268/// --- a/hello.txt
4269/// +++ b/hello.txt
4270/// @@ -1 +1 @@
4271/// -Hello, world!
4272/// +Hello, mpatch!
4273/// ```
4274/// "#;
4275/// let patch = parse_single_patch(diff_content)?;
4276///
4277/// // 3. Apply the patch to the directory.
4278/// let options = ApplyOptions::exact();
4279/// let result = apply_patch_to_file(&patch, dir.path(), options)?;
4280///
4281/// // 4. Verify the results.
4282/// assert!(result.report.all_applied_cleanly());
4283/// let new_content = fs::read_to_string(&file_path)?;
4284/// assert_eq!(new_content, "Hello, mpatch!\n");
4285/// # Ok(())
4286/// # }
4287/// ````
4288pub fn apply_patch_to_file(
4289 patch: &Patch,
4290 target_dir: &Path,
4291 options: ApplyOptions,
4292) -> Result<PatchResult, PatchError> {
4293 info!("Applying patch to: {}", patch.file_path.display());
4294
4295 // --- Path Safety Check ---
4296 // This is a critical security measure. `ensure_path_is_safe` returns a
4297 // canonicalized, absolute path that is confirmed to be inside the target_dir.
4298 let safe_target_path = ensure_path_is_safe(target_dir, &patch.file_path)?;
4299 debug!(
4300 " Resolved safe target path: '{}'",
4301 safe_target_path.display()
4302 );
4303
4304 // --- Read Original File ---
4305 // All subsequent operations use the verified `safe_target_path`.
4306 if safe_target_path.is_dir() {
4307 warn!(
4308 " Target path '{}' is a directory, not a file.",
4309 safe_target_path.display()
4310 );
4311 return Err(PatchError::TargetIsDirectory {
4312 path: safe_target_path,
4313 });
4314 }
4315
4316 let (original_content, is_new_file) = if safe_target_path.is_file() {
4317 debug!(" Target file exists. Reading content...");
4318 let content = fs::read_to_string(&safe_target_path)
4319 .map_err(|e| map_io_error(safe_target_path.clone(), e))?;
4320 trace!(
4321 " Read {} bytes ({} lines) from target file.",
4322 content.len(),
4323 content.lines().count()
4324 );
4325 (content, false)
4326 } else {
4327 // File doesn't exist. This is only okay if it's a file creation patch.
4328 if !patch.is_creation() {
4329 debug!(" Target file does not exist, and patch is not a creation patch. Aborting.");
4330 // For user-facing errors, show the original path, not the canonicalized one.
4331 return Err(PatchError::TargetNotFound(
4332 target_dir.join(&patch.file_path),
4333 ));
4334 }
4335 debug!(" Target file does not exist. Assuming file creation.");
4336 (String::new(), true)
4337 };
4338
4339 // --- Apply Patch to Content ---
4340 debug!(" Applying patch logic to content in-memory...");
4341 let result = apply_patch_to_content(
4342 patch,
4343 if is_new_file {
4344 None
4345 } else {
4346 Some(&original_content)
4347 },
4348 &options,
4349 );
4350 let new_content = result.new_content;
4351 let apply_result = result.report;
4352
4353 let mut diff = None;
4354 if options.dry_run {
4355 // In dry-run mode, generate a diff instead of writing to the file.
4356 info!(
4357 " DRY RUN: Would write changes to '{}'",
4358 patch.file_path.display()
4359 );
4360 trace!(" Generating diff for dry run...");
4361
4362 let a_path = format!("a/{}", patch.file_path.display());
4363 let b_path = format!("b/{}", patch.file_path.display());
4364 let diff_text = unified_diff(
4365 similar::Algorithm::default(),
4366 &original_content,
4367 &new_content,
4368 3,
4369 Some((&a_path, &b_path)),
4370 );
4371 diff = Some(diff_text.to_string());
4372 } else {
4373 // Write the modified content to the file system.
4374 // The parent directory might have been created by `ensure_path_is_safe`
4375 // for a new file, but we ensure it again just in case.
4376 if new_content.is_empty() {
4377 if safe_target_path.exists() {
4378 info!(
4379 " Resulting content is empty. Removing file '{}'",
4380 patch.file_path.display()
4381 );
4382 fs::remove_file(&safe_target_path)
4383 .map_err(|e| map_io_error(safe_target_path.clone(), e))?;
4384 } else {
4385 info!(
4386 " Resulting content is empty. Skipping creation of '{}'",
4387 patch.file_path.display()
4388 );
4389 }
4390
4391 if apply_result.all_applied_cleanly() {
4392 info!(
4393 " Successfully processed deletion/empty-result for '{}'",
4394 patch.file_path.display()
4395 );
4396 } else {
4397 warn!(
4398 " Partial application resulted in empty content for '{}'",
4399 patch.file_path.display()
4400 );
4401 }
4402 } else {
4403 if let Some(parent) = safe_target_path.parent() {
4404 fs::create_dir_all(parent).map_err(|e| map_io_error(parent.to_path_buf(), e))?;
4405 }
4406 trace!(
4407 " Writing {} bytes to '{}'",
4408 new_content.len(),
4409 safe_target_path.display()
4410 );
4411 fs::write(&safe_target_path, new_content)
4412 .map_err(|e| map_io_error(safe_target_path.clone(), e))?;
4413 if apply_result.all_applied_cleanly() {
4414 info!(
4415 " Successfully wrote changes to '{}'",
4416 patch.file_path.display()
4417 );
4418 } else {
4419 warn!(" Wrote partial changes to '{}'", patch.file_path.display());
4420 }
4421 }
4422 }
4423
4424 Ok(PatchResult {
4425 report: apply_result,
4426 diff,
4427 })
4428}
4429
4430/// A strict variant of [`apply_patch_to_file()`] that treats partial applications as an error.
4431///
4432/// This function provides a simpler error handling model for workflows where any
4433/// failed hunk should be considered a failure for the entire operation.
4434///
4435/// # Arguments
4436///
4437/// * `patch` - The [`Patch`] object to apply.
4438/// * `target_dir` - The base directory where the patch should be applied.
4439/// * `options` - Configuration for the patch operation.
4440///
4441/// # Returns
4442///
4443/// A [`PatchResult`] if all hunks were applied successfully.
4444///
4445/// # Errors
4446///
4447/// - Returns `Err(`[`StrictApplyError::PartialApply`]`)` if some hunks failed to apply. The file may
4448/// be in a partially patched state (unless in dry-run mode). The `report` within
4449/// the error contains the detailed results.
4450/// - Returns `Err(`[`StrictApplyError::Patch`]`)` for "hard" errors like I/O problems or a missing target file.
4451///
4452/// # Examples
4453///
4454/// ````rust
4455/// use mpatch::{parse_single_patch, try_apply_patch_to_file, ApplyOptions, StrictApplyError};
4456/// use std::fs;
4457/// use tempfile::tempdir;
4458///
4459/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4460/// // --- Success Case ---
4461/// let dir = tempdir()?;
4462/// let file_path = dir.path().join("hello.txt");
4463/// fs::write(&file_path, "Hello, world!\n")?;
4464///
4465/// let success_diff = r#"
4466/// ```diff
4467/// --- a/hello.txt
4468/// +++ b/hello.txt
4469/// @@ -1 +1 @@
4470/// -Hello, world!
4471/// +Hello, mpatch!
4472/// ```
4473/// "#;
4474/// let patch = parse_single_patch(success_diff)?;
4475///
4476/// let options = ApplyOptions::new();
4477/// let result = try_apply_patch_to_file(&patch, dir.path(), options)?;
4478/// assert!(result.report.all_applied_cleanly());
4479///
4480/// // --- Failure Case (Partial Apply) ---
4481/// let dir_fail = tempdir()?;
4482/// let file_path_fail = dir_fail.path().join("partial.txt");
4483/// fs::write(&file_path_fail, "line 1\nline 2\n")?;
4484///
4485/// let failing_diff = r#"
4486/// ```diff
4487/// --- a/partial.txt
4488/// +++ b/partial.txt
4489/// @@ -1,2 +1,2 @@
4490/// line 1
4491/// -WRONG CONTEXT
4492/// +line two
4493/// ```
4494/// "#;
4495/// let patch_fail = parse_single_patch(failing_diff)?;
4496///
4497/// let result = try_apply_patch_to_file(&patch_fail, dir_fail.path(), options);
4498/// assert!(matches!(result, Err(StrictApplyError::PartialApply { .. })));
4499///
4500/// if let Err(StrictApplyError::PartialApply { report }) = result {
4501/// assert!(!report.all_applied_cleanly());
4502/// assert_eq!(report.failures().len(), 1);
4503/// }
4504/// # Ok(())
4505/// # }
4506/// ````
4507pub fn try_apply_patch_to_file(
4508 patch: &Patch,
4509 target_dir: &Path,
4510 options: ApplyOptions,
4511) -> Result<PatchResult, StrictApplyError> {
4512 // This line was already correct
4513 let result = apply_patch_to_file(patch, target_dir, options)?;
4514 if result.report.all_applied_cleanly() {
4515 Ok(result)
4516 } else {
4517 Err(StrictApplyError::PartialApply {
4518 report: result.report,
4519 })
4520 }
4521}
4522
4523/// An iterator that applies hunks from a patch one by one.
4524///
4525/// This struct provides fine-grained control over the patch application process.
4526/// It allows you to apply hunks sequentially, inspect the intermediate state of
4527/// the content, and handle results on a per-hunk basis.
4528///
4529/// The iterator yields a [`HunkApplyStatus`] for each hunk in the patch.
4530///
4531/// # Examples
4532///
4533/// ````rust
4534/// use mpatch::{parse_single_patch, HunkApplier, HunkApplyStatus, ApplyOptions};
4535///
4536/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4537/// // 1. Define original content and a patch.
4538/// let original_lines = vec!["line 1", "line 2", "line 3"];
4539/// let diff_content = r#"
4540/// ```diff
4541/// --- a/file.txt
4542/// +++ b/file.txt
4543/// @@ -2,1 +2,1 @@
4544/// -line 2
4545/// +line two
4546/// ```
4547/// "#;
4548/// let patch = parse_single_patch(diff_content)?;
4549/// let options = ApplyOptions::new();
4550///
4551/// // 2. Create the applier.
4552/// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4553///
4554/// // 3. Apply the first (and only) hunk.
4555/// let status = applier.next().unwrap();
4556/// assert!(matches!(status, HunkApplyStatus::Applied { .. }));
4557///
4558/// // 4. Check that there are no more hunks.
4559/// assert!(applier.next().is_none());
4560///
4561/// // 5. Finalize the content.
4562/// let new_content = applier.into_content();
4563/// assert_eq!(new_content, "line 1\nline two\nline 3\n");
4564/// # Ok(())
4565/// # }
4566/// ````
4567#[derive(Debug)]
4568pub struct HunkApplier<'a> {
4569 hunks: std::slice::Iter<'a, Hunk>,
4570 current_lines: Vec<String>,
4571 options: &'a ApplyOptions,
4572 patch_ends_with_newline: bool,
4573 original_ends_with_newline: bool,
4574 touched_eof: bool,
4575}
4576
4577impl<'a> HunkApplier<'a> {
4578 /// Creates a new `HunkApplier` to begin a step-by-step patch operation.
4579 ///
4580 /// This constructor initializes the applier with the patch to be applied and the
4581 /// original content. The content is provided as an optional slice of lines,
4582 /// allowing for both file modifications (`Some(lines)`) and file creations (`None`).
4583 /// The applier can then be used as an iterator to apply hunks one by one.
4584 ///
4585 /// # Arguments
4586 ///
4587 /// * `patch` - The [`Patch`] to apply.
4588 /// * `original_lines` - An optional slice of strings representing the original content.
4589 /// * `options` - Configuration for the patch operation.
4590 ///
4591 /// # Returns
4592 ///
4593 /// A new `HunkApplier` instance.
4594 ///
4595 /// # Examples
4596 ///
4597 /// ```
4598 /// # use mpatch::{parse_single_patch, HunkApplier, ApplyOptions};
4599 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4600 /// let original_lines = vec!["line 1", "line 2"];
4601 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -2,1 +2,1\n-line 2\n+line two\n```";
4602 /// let patch = parse_single_patch(diff)?;
4603 /// let options = ApplyOptions::new();
4604 ///
4605 /// // Create the applier for a step-by-step operation.
4606 /// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4607 ///
4608 /// // Now `applier` is ready to be used as an iterator.
4609 /// let status = applier.next().unwrap();
4610 /// # Ok(())
4611 /// # }
4612 /// ```
4613 pub fn new<T: AsRef<str>>(
4614 patch: &'a Patch,
4615 original_lines: Option<&'a [T]>,
4616 options: &'a ApplyOptions,
4617 ) -> Self {
4618 let current_lines: Vec<String> = original_lines
4619 .map(|lines| lines.iter().map(|s| s.as_ref().to_string()).collect())
4620 .unwrap_or_default();
4621 Self {
4622 hunks: patch.hunks.iter(),
4623 current_lines,
4624 options,
4625 patch_ends_with_newline: patch.ends_with_newline,
4626 original_ends_with_newline: true,
4627 touched_eof: false,
4628 }
4629 }
4630
4631 /// Returns a slice of the current lines, reflecting all hunks applied so far.
4632 ///
4633 /// This method provides read-only access to the intermediate state of the
4634 /// content being patched. It is useful for inspecting the content between
4635 /// applying hunks with the `HunkApplier` iterator.
4636 ///
4637 /// # Returns
4638 ///
4639 /// A slice of strings representing the current state of the content.
4640 ///
4641 /// # Examples
4642 ///
4643 /// ```rust
4644 /// # use mpatch::{parse_single_patch, HunkApplier, ApplyOptions};
4645 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4646 /// let original_lines = vec!["line 1", "line 2"];
4647 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -2,1 +2,1\n-line 2\n+line two\n```";
4648 /// let patch = parse_single_patch(diff)?;
4649 /// let options = ApplyOptions::new();
4650 ///
4651 /// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4652 ///
4653 /// // Before applying, it's the original content.
4654 /// assert_eq!(applier.current_lines(), &["line 1", "line 2"]);
4655 ///
4656 /// // Apply the hunk.
4657 /// applier.next();
4658 ///
4659 /// // After applying, the lines are updated.
4660 /// assert_eq!(applier.current_lines(), &["line 1", "line two"]);
4661 /// # Ok(())
4662 /// # }
4663 /// ```
4664 pub fn current_lines(&self) -> &[String] {
4665 &self.current_lines
4666 }
4667
4668 /// Sets whether the original content ended with a newline.
4669 ///
4670 /// When working with a slice of lines (e.g., `Vec<String>`), the information about
4671 /// whether the original file ended with a newline character is typically lost.
4672 /// This method allows you to restore that context.
4673 ///
4674 /// `mpatch` uses this information to determine the newline status of the final output:
4675 /// 1. If a patch modifies the end of the file (i.e., the last hunk applies to the
4676 /// very end), the patch's `ends_with_newline` setting takes precedence.
4677 /// 2. If the patch only modifies the middle of the file, the original newline
4678 /// status is preserved.
4679 ///
4680 /// By default, `HunkApplier` assumes the original content ended with a newline (`true`).
4681 ///
4682 /// # Arguments
4683 ///
4684 /// * `ends_with_newline` - `true` if the original content had a trailing newline, `false` otherwise.
4685 ///
4686 /// # Returns
4687 ///
4688 /// None.
4689 ///
4690 /// # Examples
4691 ///
4692 /// ```
4693 /// # use mpatch::{parse_single_patch, HunkApplier, ApplyOptions};
4694 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4695 /// // Original content: "line 1\nline 2" (No trailing newline)
4696 /// let original_lines = vec!["line 1", "line 2"];
4697 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -1,1 +1,1\n-line 1\n+line one\n```";
4698 /// let patch = parse_single_patch(diff)?;
4699 /// let options = ApplyOptions::new();
4700 ///
4701 /// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4702 ///
4703 /// // Crucial step: Tell the applier the original file didn't have a newline.
4704 /// applier.set_original_newline_status(false);
4705 ///
4706 /// applier.next(); // Apply the hunk (modifies line 1)
4707 ///
4708 /// // The result should preserve the "no newline" status because the patch didn't touch EOF.
4709 /// let result = applier.into_content();
4710 /// assert_eq!(result, "line one\nline 2");
4711 /// # Ok(())
4712 /// # }
4713 /// ```
4714 pub fn set_original_newline_status(&mut self, ends_with_newline: bool) {
4715 self.original_ends_with_newline = ends_with_newline;
4716 }
4717
4718 /// Consumes the applier and returns the final vector of lines.
4719 ///
4720 /// After iterating through the `HunkApplier` and applying all desired hunks,
4721 /// this method can be called to take ownership of the final, modified vector
4722 /// of strings.
4723 ///
4724 /// # Returns
4725 ///
4726 /// A vector of strings representing the final content.
4727 ///
4728 /// # Examples
4729 ///
4730 /// ```rust
4731 /// # use mpatch::{parse_single_patch, HunkApplier, ApplyOptions};
4732 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4733 /// let original_lines = vec!["line 1", "line 2"];
4734 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -2,1 +2,1\n-line 2\n+line two\n```";
4735 /// let patch = parse_single_patch(diff)?;
4736 /// let options = ApplyOptions::new();
4737 ///
4738 /// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4739 /// applier.next(); // Apply all hunks
4740 ///
4741 /// let final_lines = applier.into_lines();
4742 /// assert_eq!(final_lines, vec!["line 1".to_string(), "line two".to_string()]);
4743 /// # Ok(())
4744 /// # }
4745 /// ```
4746 pub fn into_lines(self) -> Vec<String> {
4747 self.current_lines
4748 }
4749
4750 /// Consumes the applier and returns the final content as a single string.
4751 ///
4752 /// This method joins the final lines with newlines and ensures the content
4753 /// has a trailing newline if required by the patch's `ends_with_newline`
4754 /// property. It is the most common way to get the final result from a
4755 /// `HunkApplier`.
4756 ///
4757 /// # Returns
4758 ///
4759 /// A single string representing the final content.
4760 ///
4761 /// # Examples
4762 ///
4763 /// ```rust
4764 /// # use mpatch::{parse_single_patch, HunkApplier, ApplyOptions};
4765 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4766 /// let original_lines = vec!["line 1"];
4767 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -1,1 +1,1\n-line 1\n+line one\n```";
4768 /// let patch = parse_single_patch(diff)?;
4769 /// let options = ApplyOptions::new();
4770 ///
4771 /// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4772 /// applier.next(); // Apply all hunks
4773 ///
4774 /// let final_content = applier.into_content();
4775 /// assert_eq!(final_content, "line one\n");
4776 /// # Ok(())
4777 /// # }
4778 /// ```
4779 pub fn into_content(self) -> String {
4780 let mut new_content = self.current_lines.join("\n");
4781
4782 let should_have_newline = if self.touched_eof {
4783 self.patch_ends_with_newline
4784 } else {
4785 self.original_ends_with_newline
4786 };
4787
4788 if should_have_newline && !self.current_lines.is_empty() {
4789 new_content.push('\n');
4790 }
4791 new_content
4792 }
4793}
4794
4795impl<'a> Iterator for HunkApplier<'a> {
4796 type Item = HunkApplyStatus;
4797
4798 /// Applies the next hunk in the patch and returns its status.
4799 ///
4800 /// This method advances the iterator, applying one hunk to the internal state
4801 /// of the `HunkApplier`. It returns `Some(`[`HunkApplyStatus`]`)` for each hunk in
4802 /// the patch, and `None` when all hunks have been processed.
4803 ///
4804 /// # Returns
4805 ///
4806 /// An `Option` containing the [`HunkApplyStatus`] of the applied hunk, or `None` if there are no more hunks.
4807 ///
4808 /// # Examples
4809 ///
4810 /// ```
4811 /// # use mpatch::{parse_single_patch, HunkApplier, ApplyOptions, HunkApplyStatus};
4812 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4813 /// let original_lines = vec!["line 1", "line 2"];
4814 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -2,1 +2,1\n-line 2\n+line two\n```";
4815 /// let patch = parse_single_patch(diff)?;
4816 /// let options = ApplyOptions::new();
4817 ///
4818 /// let mut applier = HunkApplier::new(&patch, Some(&original_lines), &options);
4819 ///
4820 /// // Call next() to apply the first hunk.
4821 /// let status = applier.next();
4822 /// assert!(matches!(status, Some(HunkApplyStatus::Applied { .. })));
4823 ///
4824 /// // Call next() again; there are no more hunks.
4825 /// assert!(applier.next().is_none());
4826 /// # Ok(())
4827 /// # }
4828 /// ```
4829 fn next(&mut self) -> Option<Self::Item> {
4830 let hunk = self.hunks.next()?;
4831 let old_len = self.current_lines.len();
4832 let status = apply_hunk_to_lines(hunk, &mut self.current_lines, self.options);
4833
4834 if let HunkApplyStatus::Applied { location, .. } = &status {
4835 let new_len = self.current_lines.len();
4836 let delta = (new_len as isize) - (old_len as isize);
4837 let inserted_len = (location.length as isize + delta) as usize;
4838 if location.start_index + inserted_len >= new_len {
4839 self.touched_eof = true;
4840 }
4841 }
4842 Some(status)
4843 }
4844}
4845
4846/// Applies the logic of a patch to a slice of lines.
4847///
4848/// This is a high-level convenience function that drives a [`HunkApplier`] iterator
4849/// to completion and returns the final result. For more granular control, create
4850/// and use a `HunkApplier` directly.
4851///
4852/// # Arguments
4853///
4854/// * `patch` - The [`Patch`] object to apply.
4855/// * `original_lines` - An `Option` containing a slice of strings representing the file's content.
4856/// `Some(lines)` for an existing file, `None` for a new file (creation).
4857/// The slice can contain `String` or `&str`.
4858/// * `options` - Configuration for the patch operation, such as `fuzz_factor`.
4859///
4860/// # Returns
4861///
4862/// An [`InMemoryResult`] containing the new content and a detailed report.
4863///
4864/// # Examples
4865///
4866/// ```rust
4867/// # use mpatch::{parse_single_patch, apply_patch_to_lines, ApplyOptions};
4868/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4869/// // 1. Define original content and the patch.
4870/// let original_lines = vec!["Hello, world!"];
4871/// // Construct the diff string programmatically to avoid rustdoc parsing issues with ```.
4872/// let diff_str = [
4873/// "```diff",
4874/// "--- a/hello.txt",
4875/// "+++ b/hello.txt",
4876/// "@@ -1 +1 @@",
4877/// "-Hello, world!",
4878/// "+Hello, mpatch!",
4879/// "```",
4880/// ].join("\n");
4881///
4882/// // 2. Parse the diff to get a Patch object.
4883/// let patch = parse_single_patch(&diff_str)?;
4884///
4885/// // 3. Apply the patch to the lines in memory.
4886/// let options = ApplyOptions::exact();
4887/// let result = apply_patch_to_lines(&patch, Some(&original_lines), &options);
4888///
4889/// // 4. Check the results.
4890/// assert_eq!(result.new_content, "Hello, mpatch!\n");
4891/// assert!(result.report.all_applied_cleanly());
4892/// # Ok(())
4893/// # }
4894/// ```
4895pub fn apply_patch_to_lines<T: AsRef<str>>(
4896 patch: &Patch,
4897 original_lines: Option<&[T]>,
4898 options: &ApplyOptions,
4899) -> InMemoryResult {
4900 apply_patch_to_lines_internal(patch, original_lines, options, true)
4901}
4902
4903fn apply_patch_to_lines_internal<T: AsRef<str>>(
4904 patch: &Patch,
4905 original_lines: Option<&[T]>,
4906 options: &ApplyOptions,
4907 original_ends_with_newline: bool,
4908) -> InMemoryResult {
4909 debug!(
4910 " apply_patch_to_lines called with {} lines of original content.",
4911 original_lines.as_ref().map_or(0, |l| l.len())
4912 );
4913
4914 let mut applier = HunkApplier::new(patch, original_lines, options);
4915 applier.set_original_newline_status(original_ends_with_newline);
4916 let total_hunks = patch.hunks.len();
4917
4918 // Drive the iterator to completion, logging progress along the way.
4919 let hunk_results: Vec<_> = applier
4920 .by_ref()
4921 .enumerate()
4922 .map(|(i, status)| {
4923 let hunk_index = i + 1;
4924 info!(" Applying Hunk {}/{}...", hunk_index, total_hunks);
4925 match &status {
4926 HunkApplyStatus::Applied {
4927 location,
4928 match_type,
4929 replaced_lines,
4930 } => {
4931 debug!(
4932 " Successfully applied Hunk {} at {} via {:?}",
4933 hunk_index, location, match_type
4934 );
4935 if log::log_enabled!(log::Level::Trace) {
4936 trace!(" Replaced lines:");
4937 for line in replaced_lines {
4938 trace!(" - {}", line);
4939 }
4940 }
4941 }
4942 HunkApplyStatus::SkippedNoChanges => {
4943 debug!(" Skipped Hunk {} (no changes).", hunk_index);
4944 }
4945 HunkApplyStatus::Failed(error) => {
4946 warn!(" Failed to apply Hunk {}. {}", hunk_index, error);
4947 }
4948 }
4949 status
4950 })
4951 .collect();
4952
4953 // Finalize the result from the consumed applier.
4954 let new_content = applier.into_content();
4955
4956 let report = ApplyResult { hunk_results };
4957 InMemoryResult {
4958 new_content,
4959 report,
4960 }
4961}
4962
4963/// A strict variant of [`apply_patch_to_lines()`] that treats partial applications as an error.
4964///
4965/// This function provides a simpler error handling model for workflows where any
4966/// failed hunk should be considered a failure for the entire operation.
4967///
4968/// # Arguments
4969///
4970/// * `patch` - The [`Patch`] object to apply.
4971/// * `original_lines` - An `Option` containing a slice of strings representing the file's content.
4972/// * `options` - Configuration for the patch operation.
4973///
4974/// # Returns
4975///
4976/// An [`InMemoryResult`] if all hunks were applied successfully.
4977///
4978/// # Errors
4979///
4980/// Returns `Err(`[`StrictApplyError::PartialApply`]`)` if some hunks failed to apply. The returned
4981/// `report` within the error contains the detailed results.
4982///
4983/// # Examples
4984///
4985/// ````rust
4986/// use mpatch::{parse_single_patch, try_apply_patch_to_lines, ApplyOptions, StrictApplyError};
4987///
4988/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4989/// let original_lines = vec!["line 1", "line 2"];
4990///
4991/// // --- Success Case ---
4992/// let success_diff = r#"
4993/// ```diff
4994/// --- a/file.txt
4995/// +++ b/file.txt
4996/// @@ -1,2 +1,2 @@
4997/// line 1
4998/// -line 2
4999/// +line two
5000/// ```
5001/// "#;
5002/// let patch = parse_single_patch(success_diff)?;
5003/// let options = ApplyOptions::new();
5004/// let result = try_apply_patch_to_lines(&patch, Some(&original_lines), &options)?;
5005/// assert!(result.report.all_applied_cleanly());
5006/// assert_eq!(result.new_content, "line 1\nline two\n");
5007///
5008/// // --- Failure Case ---
5009/// let failing_diff = r#"
5010/// ```diff
5011/// --- a/file.txt
5012/// +++ b/file.txt
5013/// @@ -1,2 +1,2 @@
5014/// line 1
5015/// -WRONG CONTEXT
5016/// +line two
5017/// ```
5018/// "#;
5019/// let failing_patch = parse_single_patch(failing_diff)?;
5020/// let result = try_apply_patch_to_lines(&failing_patch, Some(&original_lines), &options);
5021///
5022/// assert!(matches!(result, Err(StrictApplyError::PartialApply { .. })));
5023/// if let Err(StrictApplyError::PartialApply { report }) = result {
5024/// assert!(!report.all_applied_cleanly());
5025/// }
5026/// # Ok(())
5027/// # }
5028/// ````
5029pub fn try_apply_patch_to_lines<T: AsRef<str>>(
5030 patch: &Patch,
5031 original_lines: Option<&[T]>,
5032 options: &ApplyOptions,
5033) -> Result<InMemoryResult, StrictApplyError> {
5034 // This line was already correct
5035 let result = apply_patch_to_lines(patch, original_lines, options);
5036 if result.report.all_applied_cleanly() {
5037 Ok(result)
5038 } else {
5039 Err(StrictApplyError::PartialApply {
5040 report: result.report,
5041 })
5042 }
5043}
5044
5045/// Applies the logic of a patch to a string content.
5046///
5047/// This is a pure function that takes the patch definition and the original content
5048/// of a file as a string, and returns the transformed content. It does not
5049/// interact with the filesystem. This is useful for testing, in-memory operations,
5050/// or integrating `mpatch`'s logic into other tools.
5051///
5052/// # Arguments
5053///
5054/// **Note:** For improved performance when content is already available as a slice
5055/// of lines, consider using [`apply_patch_to_lines()`].
5056///
5057/// * `patch` - The [`Patch`] object to apply.
5058/// * `original_content` - An `Option<&str>` representing the file's content.
5059/// `Some(content)` for an existing file, `None` for a new file (creation).
5060/// * `options` - Configuration for the patch operation, such as `fuzz_factor`.
5061///
5062/// # Returns
5063///
5064/// An [`InMemoryResult`] containing the new content and a detailed report.
5065///
5066/// # Examples
5067///
5068/// ```rust
5069/// # use mpatch::{parse_single_patch, apply_patch_to_content, ApplyOptions};
5070/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
5071/// // 1. Define original content and the patch.
5072/// let original_content = "Hello, world!\n";
5073/// // Construct the diff string programmatically to avoid rustdoc parsing issues with ```.
5074/// let diff_str = [
5075/// "```diff",
5076/// "--- a/hello.txt",
5077/// "+++ b/hello.txt",
5078/// "@@ -1 +1 @@",
5079/// "-Hello, world!",
5080/// "+Hello, mpatch!",
5081/// "```",
5082/// ].join("\n");
5083///
5084/// // 2. Parse the diff to get a Patch object.
5085/// let patch = parse_single_patch(&diff_str)?;
5086///
5087/// // 3. Apply the patch to the content in memory.
5088/// let options = ApplyOptions::exact();
5089/// let result = apply_patch_to_content(&patch, Some(original_content), &options);
5090///
5091/// // 4. Check the results.
5092/// assert_eq!(result.new_content, "Hello, mpatch!\n");
5093/// assert!(result.report.all_applied_cleanly());
5094/// # Ok(())
5095/// # }
5096/// ```
5097pub fn apply_patch_to_content(
5098 patch: &Patch,
5099 original_content: Option<&str>,
5100 options: &ApplyOptions,
5101) -> InMemoryResult {
5102 let original_lines: Option<Vec<String>> =
5103 original_content.map(|c| c.lines().map(String::from).collect());
5104 let original_ends_with_newline = original_content.is_none_or(|s| {
5105 if s.is_empty() {
5106 false
5107 } else {
5108 s.ends_with('\n')
5109 }
5110 });
5111 apply_patch_to_lines_internal(
5112 patch,
5113 original_lines.as_deref(),
5114 options,
5115 original_ends_with_newline,
5116 )
5117}
5118
5119/// A strict variant of [`apply_patch_to_content()`] that treats partial applications as an error.
5120///
5121/// This function provides a simpler error handling model for workflows where any
5122/// failed hunk should be considered a failure for the entire operation.
5123///
5124/// # Arguments
5125///
5126/// * `patch` - The [`Patch`] object to apply.
5127/// * `original_content` - An `Option<&str>` representing the file's content.
5128/// * `options` - Configuration for the patch operation.
5129///
5130/// # Returns
5131///
5132/// An [`InMemoryResult`] if all hunks were applied successfully.
5133///
5134/// # Errors
5135///
5136/// Returns `Err(`[`StrictApplyError::PartialApply`]`)` if some hunks failed to apply. The returned
5137/// `report` within the error contains the detailed results.
5138///
5139/// # Examples
5140///
5141/// ````rust
5142/// use mpatch::{parse_single_patch, try_apply_patch_to_content, ApplyOptions, StrictApplyError};
5143///
5144/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
5145/// let original_content = "line 1\nline 2\n";
5146///
5147/// // --- Success Case ---
5148/// let success_diff = r#"
5149/// ```diff
5150/// --- a/file.txt
5151/// +++ b/file.txt
5152/// @@ -1,2 +1,2 @@
5153/// line 1
5154/// -line 2
5155/// +line two
5156/// ```
5157/// "#;
5158/// let patch = parse_single_patch(success_diff)?;
5159/// let options = ApplyOptions::new();
5160/// let result = try_apply_patch_to_content(&patch, Some(original_content), &options)?;
5161/// assert!(result.report.all_applied_cleanly());
5162/// assert_eq!(result.new_content, "line 1\nline two\n");
5163///
5164/// // --- Failure Case ---
5165/// let failing_diff = r#"
5166/// ```diff
5167/// --- a/file.txt
5168/// +++ b/file.txt
5169/// @@ -1,2 +1,2 @@
5170/// line 1
5171/// -WRONG CONTEXT
5172/// +line two
5173/// ```
5174/// "#;
5175/// let failing_patch = parse_single_patch(failing_diff)?;
5176/// let result = try_apply_patch_to_content(&failing_patch, Some(original_content), &options);
5177///
5178/// assert!(matches!(result, Err(StrictApplyError::PartialApply { .. })));
5179/// if let Err(StrictApplyError::PartialApply { report }) = result {
5180/// assert!(!report.all_applied_cleanly());
5181/// }
5182/// # Ok(())
5183/// # }
5184/// ````
5185pub fn try_apply_patch_to_content(
5186 patch: &Patch,
5187 original_content: Option<&str>,
5188 options: &ApplyOptions,
5189) -> Result<InMemoryResult, StrictApplyError> {
5190 // This line was already correct
5191 let result = apply_patch_to_content(patch, original_content, options);
5192 if result.report.all_applied_cleanly() {
5193 Ok(result)
5194 } else {
5195 Err(StrictApplyError::PartialApply {
5196 report: result.report,
5197 })
5198 }
5199}
5200
5201/// A high-level, one-shot function to parse a diff and apply it to a string.
5202///
5203/// This function is the most convenient entry point for the common workflow of
5204/// taking a diff (e.g., from a markdown file) and applying it to some existing
5205/// content in memory. It combines parsing and strict application into a single call.
5206///
5207/// It performs the following steps:
5208/// 1. Parses the `diff_content` using [`parse_auto()`] (supporting Markdown,
5209/// Unified Diffs, and Conflict Markers).
5210/// 2. Ensures that exactly one `Patch` is found. If zero or more than one are
5211/// found, it returns an error.
5212/// 3. Applies the single patch to `original_content` using the strict logic of
5213/// [`try_apply_patch_to_content()`].
5214///
5215/// # Arguments
5216///
5217/// * `diff_content` - A string slice containing the diff. This can be a Markdown
5218/// code block, a raw Unified Diff, or Conflict Markers.
5219/// * `original_content` - An `Option<&str>` representing the content to be patched.
5220/// Use `Some(content)` for an existing file, or `None` for a file creation patch.
5221/// * `options` - Configuration for the patch operation, such as `fuzz_factor`.
5222///
5223/// # Returns
5224///
5225/// The new, patched content as a `String` if the patch applied cleanly.
5226///
5227/// # Errors
5228///
5229/// Returns `Err(`[`OneShotError`]`)` if any step fails, including parsing errors, finding
5230/// the wrong number of patches, or if the patch does not apply cleanly (i.e.,
5231/// any hunk fails).
5232///
5233/// # Examples
5234///
5235/// ````rust
5236/// # use mpatch::{patch_content_str, ApplyOptions, OneShotError};
5237/// # fn main() -> Result<(), OneShotError> {
5238/// // 1. Define the original content and the diff.
5239/// let original_content = "fn main() {\n println!(\"Hello, world!\");\n}\n";
5240/// let diff_content = r#"
5241/// ```diff
5242/// --- a/src/main.rs
5243/// +++ b/src/main.rs
5244/// @@ -1,3 +1,3 @@
5245/// fn main() {
5246/// - println!("Hello, world!");
5247/// + println!("Hello, mpatch!");
5248/// }
5249/// ```
5250/// "#;
5251///
5252/// // 2. Call the one-shot function.
5253/// let options = ApplyOptions::new();
5254/// let new_content = patch_content_str(diff_content, Some(original_content), &options)?;
5255///
5256/// // 3. Verify the new content.
5257/// let expected_content = "fn main() {\n println!(\"Hello, mpatch!\");\n}\n";
5258/// assert_eq!(new_content, expected_content);
5259///
5260/// Ok(())
5261/// # }
5262/// ````
5263pub fn patch_content_str(
5264 diff_content: &str,
5265 original_content: Option<&str>,
5266 options: &ApplyOptions,
5267) -> Result<String, OneShotError> {
5268 let mut patches = parse_auto(diff_content)?;
5269 if patches.is_empty() {
5270 return Err(OneShotError::NoPatchesFound);
5271 }
5272 if patches.len() > 1 {
5273 return Err(OneShotError::MultiplePatchesFound(patches.len()));
5274 }
5275 let patch = patches.remove(0);
5276 let result = try_apply_patch_to_content(&patch, original_content, options)?;
5277 Ok(result.new_content)
5278}
5279
5280/// Helper to adjust the indentation of a line based on a detected offset.
5281///
5282/// If `target_indent` is shorter than `hunk_indent`, we strip the difference from `line`.
5283/// If `target_indent` is longer, we prepend the difference.
5284fn adjust_indentation(line: &str, hunk_indent: &str, target_indent: &str) -> String {
5285 if line.trim().is_empty() {
5286 return String::new();
5287 }
5288 if hunk_indent == target_indent {
5289 return line.to_string();
5290 }
5291
5292 let line_indent = get_indent(line);
5293
5294 // Check for pure spaces to pure tabs translation (or vice versa)
5295 if !hunk_indent.is_empty() && !target_indent.is_empty() {
5296 let hunk_is_spaces = hunk_indent.chars().all(|c| c == ' ');
5297 let target_is_tabs = target_indent.chars().all(|c| c == '\t');
5298
5299 if hunk_is_spaces && target_is_tabs {
5300 let spaces_per_tab = if hunk_indent.len().is_multiple_of(target_indent.len())
5301 && hunk_indent.len() / target_indent.len() <= 4
5302 {
5303 hunk_indent.len() / target_indent.len()
5304 } else {
5305 4
5306 };
5307
5308 if line_indent.chars().all(|c| c == ' ') {
5309 let hunk_tabs = hunk_indent.len() / spaces_per_tab;
5310 let line_tabs = line_indent.len() / spaces_per_tab;
5311 let line_spaces = line_indent.len() % spaces_per_tab;
5312
5313 let target_tabs = target_indent.len();
5314
5315 if line_tabs >= hunk_tabs {
5316 let new_tabs = target_tabs + (line_tabs - hunk_tabs);
5317 let new_indent =
5318 format!("{}{}", "\t".repeat(new_tabs), " ".repeat(line_spaces));
5319 return format!("{}{}", new_indent, &line[line_indent.len()..]);
5320 } else {
5321 let outdent = hunk_tabs - line_tabs;
5322 let new_tabs = target_tabs.saturating_sub(outdent);
5323 let new_indent =
5324 format!("{}{}", "\t".repeat(new_tabs), " ".repeat(line_spaces));
5325 return format!("{}{}", new_indent, &line[line_indent.len()..]);
5326 }
5327 }
5328 }
5329
5330 let hunk_is_tabs = hunk_indent.chars().all(|c| c == '\t');
5331 let target_is_spaces = target_indent.chars().all(|c| c == ' ');
5332
5333 if hunk_is_tabs && target_is_spaces {
5334 let spaces_per_tab = if target_indent.len().is_multiple_of(hunk_indent.len())
5335 && target_indent.len() / hunk_indent.len() <= 4
5336 {
5337 target_indent.len() / hunk_indent.len()
5338 } else {
5339 4
5340 };
5341
5342 if line_indent.chars().all(|c| c == '\t') {
5343 let hunk_tabs = hunk_indent.len();
5344 let line_tabs = line_indent.len();
5345
5346 let target_spaces = target_indent.len();
5347
5348 if line_tabs >= hunk_tabs {
5349 let new_spaces = target_spaces + (line_tabs - hunk_tabs) * spaces_per_tab;
5350 let new_indent = " ".repeat(new_spaces);
5351 return format!("{}{}", new_indent, &line[line_indent.len()..]);
5352 } else {
5353 let outdent_spaces = (hunk_tabs - line_tabs) * spaces_per_tab;
5354 let new_spaces = target_spaces.saturating_sub(outdent_spaces);
5355 let new_indent = " ".repeat(new_spaces);
5356 return format!("{}{}", new_indent, &line[line_indent.len()..]);
5357 }
5358 }
5359 }
5360 }
5361
5362 // If the line starts with the hunk's indentation, we can simply replace it with the target's indentation.
5363 if let Some(stripped) = line.strip_prefix(hunk_indent) {
5364 return format!("{}{}", target_indent, stripped);
5365 }
5366
5367 // Fallback for lines that are outdented relative to the hunk's context.
5368 if let Some(diff) = hunk_indent.strip_prefix(target_indent) {
5369 // Hunk is more indented than target. Try to strip the difference from the end of line_indent.
5370 let mut new_indent = line_indent.to_string();
5371 for c in diff.chars().rev() {
5372 if new_indent.ends_with(c) {
5373 new_indent.pop();
5374 } else {
5375 break;
5376 }
5377 }
5378 format!("{}{}", new_indent, &line[line_indent.len()..])
5379 } else if let Some(diff) = target_indent.strip_prefix(hunk_indent) {
5380 // Target is more indented than hunk.
5381 // We need to add `diff` to the start of `line`.
5382 format!("{}{}", diff, line)
5383 } else {
5384 line.to_string()
5385 }
5386}
5387
5388/// Helper to extract the whitespace prefix from a line.
5389fn get_indent(line: &str) -> &str {
5390 &line[..line.len() - line.trim_start().len()]
5391}
5392
5393/// Applies a single hunk to a mutable vector of lines in-place.
5394///
5395/// This function provides granular control over the patching process, allowing library
5396/// users to apply changes hunk-by-hunk. It modifies the `target_lines` vector
5397/// directly based on the changes defined in the `hunk`.
5398///
5399/// # Arguments
5400///
5401/// * `hunk` - The [`Hunk`] to apply.
5402/// * `target_lines` - A mutable vector of strings representing the file's content.
5403/// This vector will be modified by the function.
5404/// * `options` - Configuration for the patch operation, such as `fuzz_factor`.
5405///
5406/// # Returns
5407///
5408/// A [`HunkApplyStatus`] indicating the outcome:
5409/// - [`HunkApplyStatus::Applied`]: The hunk was successfully applied. The `location` and `match_type` are provided.
5410/// - [`HunkApplyStatus::SkippedNoChanges`]: The hunk contained only context lines and was skipped.
5411/// - [`HunkApplyStatus::Failed`]: The hunk could not be applied. The reason is provided in the associated [`HunkApplyError`].
5412///
5413/// # Examples
5414///
5415/// ```rust
5416/// # use mpatch::{parse_single_patch, apply_hunk_to_lines, ApplyOptions, HunkApplyStatus, HunkLocation, MatchType};
5417/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
5418/// // 1. Define original content and the patch.
5419/// let mut original_lines = vec!["Hello, world!".to_string()];
5420/// let diff_str = [
5421/// "```diff",
5422/// "--- a/hello.txt",
5423/// "+++ b/hello.txt",
5424/// "@@ -1 +1 @@",
5425/// "-Hello, world!",
5426/// "+Hello, mpatch!",
5427/// "```",
5428/// ].join("\n");
5429///
5430/// // 2. Parse the diff to get a Hunk object.
5431/// let patch = parse_single_patch(&diff_str)?;
5432/// let hunk = &patch.hunks[0];
5433///
5434/// // 3. Apply the hunk to the lines in memory.
5435/// let options = ApplyOptions::exact();
5436/// let status = apply_hunk_to_lines(hunk, &mut original_lines, &options);
5437///
5438/// // 4. Check the results.
5439/// assert!(matches!(status, HunkApplyStatus::Applied { replaced_lines, .. } if replaced_lines == vec!["Hello, world!"]));
5440/// assert_eq!(original_lines, vec!["Hello, mpatch!"]);
5441/// # Ok(())
5442/// # }
5443/// ```
5444pub fn apply_hunk_to_lines(
5445 hunk: &Hunk,
5446 target_lines: &mut Vec<String>,
5447 options: &ApplyOptions,
5448) -> HunkApplyStatus {
5449 debug!("Applying hunk with {} lines.", hunk.lines.len());
5450 if log::log_enabled!(log::Level::Trace) {
5451 trace!(" Match block: {:?}", hunk.get_match_block());
5452 trace!(" Replace block: {:?}", hunk.get_replace_block());
5453 }
5454 if !hunk.has_changes() {
5455 debug!(" Hunk has no changes (only context lines), skipping.");
5456 return HunkApplyStatus::SkippedNoChanges;
5457 }
5458
5459 match find_hunk_location_in_lines(hunk, target_lines, options) {
5460 Ok((location, match_type)) => {
5461 debug!(
5462 " Found location {:?} with match type {:?}. Applying changes.",
5463 location, match_type
5464 );
5465
5466 let final_replace_block: Vec<String> = if matches!(match_type, MatchType::Exact) {
5467 // For Exact matches, we assume the patch's indentation is intentional and correct relative to the context.
5468 // We don't need dynamic adjustment because the context matched byte-for-byte.
5469 trace!(" Applying hunk via exact logic.");
5470 hunk.get_replace_block()
5471 .iter()
5472 .map(|s| {
5473 if s.trim().is_empty() {
5474 String::new()
5475 } else {
5476 s.to_string()
5477 }
5478 })
5479 .collect()
5480 } else {
5481 // For Fuzzy and ExactIgnoringWhitespace, indentation might mismatch or drift.
5482 // We use a robust reconstruction that dynamically adjusts indentation based on the
5483 // nearest matching line.
5484 debug!(" Applying hunk via robust reconstruction logic (preserving file context & adjusting indent).");
5485 trace!(
5486 " Fuzzy match location: start={}, len={}",
5487 location.start_index,
5488 location.length
5489 );
5490 let file_matched_lines: Vec<_> = target_lines
5491 [location.start_index..location.start_index + location.length]
5492 .to_vec();
5493 trace!(
5494 " File content in matched range: {:?}",
5495 file_matched_lines
5496 );
5497
5498 // 1. Parse hunk to separate match lines and additions.
5499 // We map each line in the match block (Context/Removal) to a list of additions that follow it.
5500 // match_lines_meta: Vec<(is_removal, additions_after_this_line)>
5501 // Note: We store raw additions here and adjust them later during reconstruction.
5502 let mut match_lines_meta: Vec<(bool, Vec<String>)> = Vec::new();
5503 let mut initial_additions: Vec<String> = Vec::new();
5504
5505 let mut line_iter = hunk.lines.iter().peekable();
5506
5507 // Consume any additions that appear before the first context/removal line
5508 while let Some(line) = line_iter.peek() {
5509 if let Some(stripped) = line.strip_prefix('+') {
5510 initial_additions.push(stripped.to_string());
5511 line_iter.next();
5512 } else {
5513 break;
5514 }
5515 }
5516
5517 // Process the rest of the hunk
5518 for line in line_iter {
5519 if let Some(stripped) = line.strip_prefix('+') {
5520 // Attach this addition to the most recent match line
5521 if let Some(last) = match_lines_meta.last_mut() {
5522 last.1.push(stripped.to_string());
5523 } else {
5524 // Should be unreachable if match block is not empty, but safe fallback
5525 initial_additions.push(stripped.to_string());
5526 }
5527 } else {
5528 // It's a Context (' ') or Removal ('-') line
5529 let is_removal = line.starts_with('-');
5530 match_lines_meta.push((is_removal, Vec::new()));
5531 }
5532 }
5533
5534 // 2. Prepare text for diffing
5535 // We align the hunk's "old" view (match block) with the file's actual content.
5536 let match_block_content: Vec<&str> = hunk.get_match_block();
5537 let file_block_content: Vec<&str> =
5538 file_matched_lines.iter().map(|s| s.as_str()).collect();
5539
5540 let match_block_trimmed: Vec<&str> =
5541 match_block_content.iter().map(|s| s.trim()).collect();
5542 let file_block_trimmed: Vec<&str> =
5543 file_block_content.iter().map(|s| s.trim()).collect();
5544
5545 // 3. Diff
5546 let diff =
5547 similar::TextDiff::from_slices(&match_block_trimmed, &file_block_trimmed);
5548
5549 // 4. Determine Initial Indentation Context
5550 // We scan the diff ops to find the first aligned line (Equal or Replace)
5551 // to establish the baseline indentation difference.
5552 let mut current_hunk_indent = "";
5553 let mut current_target_indent = "";
5554
5555 for op in diff.ops() {
5556 let mut found = false;
5557 match op {
5558 similar::DiffOp::Equal {
5559 old_index,
5560 new_index,
5561 len,
5562 } => {
5563 // Use the first non-empty line of the block to gauge indentation
5564 for i in 0..*len {
5565 let h_line = match_block_content[*old_index + i];
5566 let t_line = file_block_content[*new_index + i];
5567 let h_ind = get_indent(h_line);
5568 let t_ind = get_indent(t_line);
5569 if (!h_ind.is_empty() || !t_ind.is_empty())
5570 && !h_line.trim().is_empty()
5571 && !t_line.trim().is_empty()
5572 {
5573 current_hunk_indent = h_ind;
5574 current_target_indent = t_ind;
5575 trace!(
5576 " Initial Indentation Context: Hunk='{}', Target='{}'",
5577 h_ind.escape_debug(),
5578 t_ind.escape_debug()
5579 );
5580 found = true;
5581 break;
5582 }
5583 }
5584 }
5585 similar::DiffOp::Replace {
5586 old_index,
5587 new_index,
5588 old_len,
5589 new_len,
5590 } => {
5591 let min_len = std::cmp::min(*old_len, *new_len);
5592 for i in 0..min_len {
5593 let h_line = match_block_content[*old_index + i];
5594 let t_line = file_block_content[*new_index + i];
5595 let h_ind = get_indent(h_line);
5596 let t_ind = get_indent(t_line);
5597 if (!h_ind.is_empty() || !t_ind.is_empty())
5598 && !h_line.trim().is_empty()
5599 && !t_line.trim().is_empty()
5600 {
5601 current_hunk_indent = h_ind;
5602 current_target_indent = t_ind;
5603 trace!(" Initial Indentation Context (from Replace): Hunk='{}', Target='{}'", h_ind.escape_debug(), t_ind.escape_debug());
5604 found = true;
5605 break;
5606 }
5607 }
5608 }
5609 _ => {}
5610 }
5611 if found {
5612 break;
5613 }
5614 }
5615
5616 // 5. Reconstruct the block
5617 let mut final_lines = Vec::new();
5618
5619 // Apply initial additions using the seeded indentation
5620 for line in initial_additions {
5621 final_lines.push(adjust_indentation(
5622 &line,
5623 current_hunk_indent,
5624 current_target_indent,
5625 ));
5626 }
5627
5628 let is_at_eof = (location.start_index + location.length) == target_lines.len();
5629 let ops = diff.ops().to_vec();
5630
5631 for (op_idx, op) in ops.iter().enumerate() {
5632 match op {
5633 similar::DiffOp::Equal {
5634 old_index,
5635 new_index,
5636 len,
5637 } => {
5638 // The file content matches the hunk's expectation (fuzzy or exact).
5639 for i in 0..*len {
5640 let old_idx = old_index + i;
5641 let new_idx = new_index + i;
5642 let (is_removal, additions) = &match_lines_meta[old_idx];
5643
5644 // Update indentation context dynamically based on this matching line
5645 let h_line = match_block_content[old_idx];
5646 let t_line = &file_matched_lines[new_idx];
5647 let h_ind = get_indent(h_line);
5648 let t_ind = get_indent(t_line);
5649 if (!h_ind.is_empty() || !t_ind.is_empty())
5650 && !h_line.trim().is_empty()
5651 && !t_line.trim().is_empty()
5652 && (current_hunk_indent != h_ind
5653 || current_target_indent != t_ind)
5654 {
5655 trace!(" Dynamic Indentation Update (Equal): Hunk='{}', Target='{}'", h_ind.escape_debug(), t_ind.escape_debug());
5656 current_hunk_indent = h_ind;
5657 current_target_indent = t_ind;
5658 }
5659
5660 // If it's not a removal, keep the file's version of the line (preserves local edits)
5661 if !*is_removal {
5662 final_lines.push(file_matched_lines[new_idx].clone());
5663 }
5664 // Always insert the additions associated with this line
5665 for add in additions {
5666 final_lines.push(adjust_indentation(
5667 add,
5668 current_hunk_indent,
5669 current_target_indent,
5670 ));
5671 }
5672 }
5673 }
5674 similar::DiffOp::Delete {
5675 old_index, old_len, ..
5676 } => {
5677 // Lines in hunk match block that are missing in the file.
5678 // If it was a REMOVAL line, it's already gone, so we skip it.
5679 // If it was a CONTEXT line, we only restore it if we are at the EOF
5680 // and this is the trailing part of the patch (implying truncation).
5681 // Otherwise, we assume it's stale context (extra line in patch) and skip it.
5682 let is_last_op = op_idx == ops.len() - 1;
5683 for i in 0..*old_len {
5684 let old_idx = old_index + i;
5685 let (is_removal, additions) = &match_lines_meta[old_idx];
5686 if !*is_removal && is_at_eof && is_last_op {
5687 // Restore truncated context at EOF
5688 let line = match_block_content[old_idx];
5689 // Adjust it to match target style? Best effort using last known.
5690 final_lines.push(adjust_indentation(
5691 line,
5692 current_hunk_indent,
5693 current_target_indent,
5694 ));
5695 }
5696 for add in additions {
5697 final_lines.push(adjust_indentation(
5698 add,
5699 current_hunk_indent,
5700 current_target_indent,
5701 ));
5702 }
5703 }
5704 }
5705 similar::DiffOp::Insert {
5706 new_index, new_len, ..
5707 } => {
5708 // Extra lines in the file (local insertions).
5709 // We preserve them.
5710 for i in 0..*new_len {
5711 let new_idx = new_index + i;
5712 final_lines.push(file_matched_lines[new_idx].clone());
5713 }
5714 }
5715 similar::DiffOp::Replace {
5716 old_index,
5717 old_len,
5718 new_index,
5719 new_len,
5720 } => {
5721 // A region where the file differs significantly from the hunk.
5722 // Try to update indentation from the first non-empty line of the replacement block
5723 if *old_len > 0 && *new_len > 0 {
5724 let min_len = std::cmp::min(*old_len, *new_len);
5725 for i in 0..min_len {
5726 let h_line = match_block_content[*old_index + i];
5727 let t_line = &file_matched_lines[*new_index + i];
5728 let h_ind = get_indent(h_line);
5729 let t_ind = get_indent(t_line);
5730 if (!h_ind.is_empty() || !t_ind.is_empty())
5731 && !h_line.trim().is_empty()
5732 && !t_line.trim().is_empty()
5733 {
5734 if current_hunk_indent != h_ind
5735 || current_target_indent != t_ind
5736 {
5737 trace!(" Dynamic Indentation Update (Replace search): Hunk='{}', Target='{}'", h_ind.escape_debug(), t_ind.escape_debug());
5738 current_hunk_indent = h_ind;
5739 current_target_indent = t_ind;
5740 }
5741 break;
5742 }
5743 }
5744 }
5745
5746 // If lengths match, we assume a 1-to-1 correspondence (e.g. whitespace changes).
5747 if *old_len == *new_len {
5748 for i in 0..*old_len {
5749 let old_idx = old_index + i;
5750 let new_idx = new_index + i;
5751 let (is_removal, additions) = &match_lines_meta[old_idx];
5752
5753 let h_line = match_block_content[old_idx];
5754 let t_line = &file_matched_lines[new_idx];
5755 let h_ind = get_indent(h_line);
5756 let t_ind = get_indent(t_line);
5757 if (!h_ind.is_empty() || !t_ind.is_empty())
5758 && !h_line.trim().is_empty()
5759 && !t_line.trim().is_empty()
5760 && (current_hunk_indent != h_ind
5761 || current_target_indent != t_ind)
5762 {
5763 trace!(" Dynamic Indentation Update (Replace match): Hunk='{}', Target='{}'", h_ind.escape_debug(), t_ind.escape_debug());
5764 current_hunk_indent = h_ind;
5765 current_target_indent = t_ind;
5766 }
5767
5768 if !*is_removal {
5769 final_lines.push(file_matched_lines[new_idx].clone());
5770 }
5771 for add in additions {
5772 final_lines.push(adjust_indentation(
5773 add,
5774 current_hunk_indent,
5775 current_target_indent,
5776 ));
5777 }
5778 }
5779 } else {
5780 // Heuristic: If the hunk region contains ANY context lines, we assume
5781 // the file content is a modified version of that context, so we KEEP it.
5782 // If the hunk region is PURELY removals, we assume the file content
5783 // is what needs to be removed, so we DROP it.
5784 let mut has_context = false;
5785 for i in 0..*old_len {
5786 if !match_lines_meta[old_index + i].0 {
5787 has_context = true;
5788 break;
5789 }
5790 }
5791
5792 if has_context {
5793 for i in 0..*new_len {
5794 final_lines.push(file_matched_lines[new_index + i].clone());
5795 }
5796 }
5797
5798 // Always append additions associated with the old lines
5799 for i in 0..*old_len {
5800 let (_, additions) = &match_lines_meta[old_index + i];
5801 for add in additions {
5802 final_lines.push(adjust_indentation(
5803 add,
5804 current_hunk_indent,
5805 current_target_indent,
5806 ));
5807 }
5808 }
5809 }
5810 }
5811 }
5812 }
5813 final_lines
5814 };
5815
5816 let replaced_lines: Vec<String> = target_lines
5817 .splice(
5818 location.start_index..location.start_index + location.length,
5819 final_replace_block,
5820 )
5821 .collect();
5822 trace!(
5823 " Successfully spliced changes into target lines. Replaced {} lines.",
5824 replaced_lines.len()
5825 );
5826 HunkApplyStatus::Applied {
5827 location,
5828 match_type,
5829 replaced_lines,
5830 }
5831 }
5832 Err(error) => {
5833 // The calling function will log the failure with context (e.g., hunk index).
5834 HunkApplyStatus::Failed(error)
5835 }
5836 }
5837}
5838
5839/// A trait for strategies that find the location to apply a hunk.
5840///
5841/// This allows the core matching algorithm to be pluggable, enabling different
5842/// search strategies to be used if needed.
5843/// The library provides a robust [`DefaultHunkFinder`] that should be sufficient
5844/// for most use cases.
5845///
5846/// # Arguments
5847///
5848/// * `hunk` - The hunk to locate.
5849/// * `target_lines` - The content to search within.
5850///
5851/// # Returns
5852///
5853/// A tuple containing the [`HunkLocation`] and the [`MatchType`] on success.
5854///
5855/// # Errors
5856///
5857/// Returns `Err(`[`HunkApplyError`]`)` if no suitable location could be found.
5858///
5859/// # Examples
5860///
5861/// This example shows a hypothetical, simplified implementation of a `HunkFinder`
5862/// that only performs an exact match search.
5863///
5864/// ```
5865/// use mpatch::{Hunk, HunkFinder, HunkLocation, MatchType, HunkApplyError};
5866///
5867/// struct ExactOnlyFinder;
5868///
5869/// impl HunkFinder for ExactOnlyFinder {
5870/// fn find_location<T: AsRef<str> + Sync>(
5871/// &self,
5872/// hunk: &Hunk,
5873/// target_lines: &[T],
5874/// ) -> Result<(HunkLocation, MatchType), HunkApplyError> {
5875/// let match_block = hunk.get_match_block();
5876/// if match_block.is_empty() {
5877/// return Err(HunkApplyError::ContextNotFound);
5878/// }
5879///
5880/// target_lines
5881/// .windows(match_block.len())
5882/// .enumerate()
5883/// .find(|(_, window)| {
5884/// window.iter().map(|s| s.as_ref()).eq(match_block.iter().copied())
5885/// })
5886/// .map(|(i, _)| (
5887/// HunkLocation { start_index: i, length: match_block.len() },
5888/// MatchType::Exact
5889/// ))
5890/// .ok_or(HunkApplyError::ContextNotFound)
5891/// }
5892/// }
5893/// ```
5894pub trait HunkFinder {
5895 /// Finds the location to apply a hunk to a slice of lines.
5896 ///
5897 /// This is the core method for any `HunkFinder` implementation. It encapsulates
5898 /// the search logic used to determine where a hunk's changes should be applied
5899 /// within the target content. Implementations should return the location and the
5900 /// type of match found, or an error if no suitable location can be determined.
5901 ///
5902 /// # Arguments
5903 ///
5904 /// * `hunk` - The [`Hunk`] to locate.
5905 /// * `target_lines` - A slice of strings representing the content to search within.
5906 ///
5907 /// # Returns
5908 ///
5909 /// A tuple containing the [`HunkLocation`] and the [`MatchType`] on success.
5910 ///
5911 /// # Errors
5912 ///
5913 /// Returns `Err(`[`HunkApplyError`]`)` if no suitable location could be found.
5914 ///
5915 /// # Examples
5916 ///
5917 /// ```
5918 /// # use mpatch::{parse_single_patch, DefaultHunkFinder, HunkFinder, ApplyOptions, HunkLocation, MatchType};
5919 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
5920 /// // 1. Create a hunk to search for.
5921 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -1,2 +1,2\n line 1\n-line 2\n+line two\n```";
5922 /// let hunk = parse_single_patch(diff)?.hunks.remove(0);
5923 ///
5924 /// // 2. Define the content to search within.
5925 /// let target_lines = vec!["line 1", "line 2"];
5926 ///
5927 /// // 3. Instantiate a finder and call the method.
5928 /// let options = ApplyOptions::new();
5929 /// let finder = DefaultHunkFinder::new(&options);
5930 /// let (location, match_type) = finder.find_location(&hunk, &target_lines)?;
5931 ///
5932 /// // 4. Check the result.
5933 /// assert_eq!(location, HunkLocation { start_index: 0, length: 2 });
5934 /// assert!(matches!(match_type, MatchType::Exact));
5935 /// # Ok(())
5936 /// # }
5937 /// ```
5938 fn find_location<T: AsRef<str> + Sync>(
5939 &self,
5940 hunk: &Hunk,
5941 target_lines: &[T],
5942 ) -> Result<(HunkLocation, MatchType), HunkApplyError>;
5943}
5944
5945/// The default, built-in strategy for finding hunk locations.
5946///
5947/// This implementation uses a hierarchical approach:
5948/// 1. Exact match.
5949/// 2. Exact match ignoring trailing whitespace.
5950/// 3. Flexible fuzzy match using a similarity algorithm.
5951///
5952/// It uses line number hints from the patch to resolve ambiguities.
5953/// While you can use this struct directly, it's typically used internally by
5954/// functions like [`find_hunk_location_in_lines()`].
5955///
5956/// # Examples
5957///
5958/// ````rust
5959/// # use mpatch::{parse_single_patch, DefaultHunkFinder, HunkFinder, ApplyOptions, HunkLocation, MatchType};
5960/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
5961/// let original_lines = vec!["line 1", "line two", "line 3"];
5962/// let diff = r#"
5963/// ```diff
5964/// --- a/file.txt
5965/// +++ b/file.txt
5966/// @@ -1,3 +1,3 @@
5967/// line 1
5968/// -line two
5969/// +line 2
5970/// line 3
5971/// ```
5972/// "#;
5973/// let hunk = parse_single_patch(diff)?.hunks.remove(0);
5974///
5975/// let options = ApplyOptions::exact();
5976/// let finder = DefaultHunkFinder::new(&options);
5977///
5978/// // Use the finder to locate the hunk.
5979/// let (location, match_type) = finder.find_location(&hunk, &original_lines)?;
5980///
5981/// assert_eq!(location, HunkLocation { start_index: 0, length: 3 });
5982/// assert!(matches!(match_type, MatchType::Exact));
5983/// # Ok(())
5984/// # }
5985/// ````
5986#[derive(Debug)]
5987pub struct DefaultHunkFinder<'a> {
5988 options: &'a ApplyOptions,
5989}
5990
5991impl<'a> DefaultHunkFinder<'a> {
5992 /// Creates a new finder with the given options.
5993 ///
5994 /// This is the standard way to instantiate the `DefaultHunkFinder`. The provided
5995 /// [`ApplyOptions`] will control the behavior of the finder, particularly the
5996 /// `fuzz_factor` which determines the threshold for fuzzy matching.
5997 ///
5998 /// # Arguments
5999 ///
6000 /// * `options` - Configuration for the patch operation.
6001 ///
6002 /// # Returns
6003 ///
6004 /// A new `DefaultHunkFinder` instance.
6005 ///
6006 /// # Examples
6007 ///
6008 /// ```
6009 /// # use mpatch::{DefaultHunkFinder, ApplyOptions};
6010 /// // Create a finder that requires a high similarity for fuzzy matches.
6011 /// let options = ApplyOptions::new().with_fuzz_factor(0.9);
6012 /// let finder = DefaultHunkFinder::new(&options);
6013 /// ```
6014 pub fn new(options: &'a ApplyOptions) -> Self {
6015 Self { options }
6016 }
6017
6018 /// Finds optimized search ranges within the target file to perform the fuzzy search.
6019 ///
6020 /// This is a performance heuristic. It tries to find an "anchor" line from the
6021 /// hunk that is relatively uncommon in the target file. If successful, it returns
6022 /// small search windows around the occurrences of that anchor. If no good anchor
6023 /// is found, it returns a single range covering the entire file.
6024 fn find_search_ranges<T: AsRef<str>>(
6025 match_block: &[&str],
6026 target_lines: &[T],
6027 hunk_size: usize,
6028 ) -> Vec<(usize, usize)> {
6029 const MAX_ANCHOR_OCCURRENCES: usize = 5;
6030 const MIN_ANCHOR_LEN: usize = 5;
6031 // Search radius is this factor times the hunk size, with a minimum.
6032 const SEARCH_RADIUS_FACTOR: usize = 2;
6033 const MIN_SEARCH_RADIUS: usize = 15;
6034
6035 if hunk_size == 0 {
6036 return vec![(0, target_lines.len())];
6037 }
6038
6039 // Iterate from the middle of the hunk outwards to find a good anchor line.
6040 // The middle is often more stable than the edges.
6041 let mid = hunk_size / 2;
6042 for i in 0..=mid {
6043 let indices_to_check = [Some(mid + i), if i > 0 { Some(mid - i) } else { None }];
6044
6045 for &line_idx_opt in &indices_to_check {
6046 if let Some(line_idx) = line_idx_opt {
6047 if line_idx >= hunk_size {
6048 continue;
6049 }
6050
6051 let anchor_line = match_block[line_idx].trim();
6052 // Ignore short or empty lines as they are poor anchors.
6053 if anchor_line.len() < MIN_ANCHOR_LEN {
6054 continue;
6055 }
6056
6057 // Find all occurrences of the anchor line.
6058 let occurrences: Vec<_> = target_lines
6059 .iter()
6060 .enumerate()
6061 .filter(|(_, l)| l.as_ref().trim() == anchor_line)
6062 .map(|(i, _)| i)
6063 .collect();
6064
6065 // If the line is unique enough, use it to create search ranges.
6066 if !occurrences.is_empty() && occurrences.len() <= MAX_ANCHOR_OCCURRENCES {
6067 debug!(
6068 " Found good anchor line (hunk line {}) with {} occurrences.",
6069 line_idx + 1,
6070 occurrences.len(),
6071 );
6072 trace!(" Anchor text: '{}'", anchor_line);
6073 let mut ranges = Vec::new();
6074 let search_radius =
6075 (hunk_size * SEARCH_RADIUS_FACTOR).max(MIN_SEARCH_RADIUS);
6076
6077 for &occurrence_idx in &occurrences {
6078 // Estimate where the hunk would start based on the anchor's position.
6079 let estimated_start = occurrence_idx.saturating_sub(line_idx);
6080 let start = estimated_start.saturating_sub(search_radius);
6081 let end = (estimated_start + hunk_size + search_radius)
6082 .min(target_lines.len());
6083 ranges.push((start, end));
6084 }
6085 // Merge any overlapping ranges created by nearby occurrences.
6086 return Self::merge_ranges(ranges);
6087 }
6088 }
6089 }
6090 }
6091
6092 // If no good anchor was found, we must search the entire file.
6093 debug!(" No suitable anchor line found. Falling back to full file scan.");
6094 vec![(0, target_lines.len())]
6095 }
6096
6097 /// Merges a list of overlapping or adjacent ranges into a minimal set of disjoint ranges.
6098 fn merge_ranges(mut ranges: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
6099 if ranges.is_empty() {
6100 return vec![];
6101 }
6102 ranges.sort_unstable_by_key(|k| k.0);
6103 let mut merged = Vec::with_capacity(ranges.len());
6104 let mut current_range = ranges[0];
6105
6106 for &(start, end) in &ranges[1..] {
6107 if start <= current_range.1 {
6108 // Overlap or adjacent, merge them.
6109 current_range.1 = current_range.1.max(end);
6110 } else {
6111 // No overlap, push the current range and start a new one.
6112 merged.push(current_range);
6113 current_range = (start, end);
6114 }
6115 }
6116 merged.push(current_range);
6117 merged
6118 }
6119
6120 /// Finds the starting index of the hunk's match block in the target lines.
6121 /// This function implements the core hierarchical search strategy.
6122 fn find_hunk_location_internal<T: AsRef<str> + Sync>(
6123 &self,
6124 match_block: &[&str],
6125 target_lines: &[T],
6126 old_start_line: Option<usize>,
6127 ) -> Result<(HunkLocation, MatchType), HunkApplyError> {
6128 trace!(
6129 " find_hunk_location_internal called for a hunk with {} lines to match against {} target lines.",
6130 match_block.len(),
6131 target_lines.len()
6132 );
6133
6134 if match_block.is_empty() {
6135 // An empty match block (file creation) can only be applied to an empty file.
6136 trace!(" Match block is empty (file creation).");
6137 return if target_lines.is_empty() {
6138 trace!(" Target is empty, match successful at (0, 0).");
6139 Ok((
6140 HunkLocation {
6141 start_index: 0,
6142 length: 0,
6143 },
6144 MatchType::Exact,
6145 ))
6146 } else {
6147 trace!(" Target is not empty, match failed.");
6148 Err(HunkApplyError::ContextNotFound)
6149 };
6150 }
6151
6152 // --- STRATEGY 1: Exact Match ---
6153 // The fastest and most reliable method.
6154 trace!(" Attempting exact match for hunk...");
6155 {
6156 let result = if match_block.len() <= target_lines.len() {
6157 let iter = target_lines
6158 .windows(match_block.len())
6159 .enumerate()
6160 .filter(|(_, window)| {
6161 window
6162 .iter()
6163 .map(|s| s.as_ref())
6164 .eq(match_block.iter().copied())
6165 })
6166 .map(|(i, _)| i);
6167 Self::tie_break_with_line_number(iter, old_start_line, "exact")
6168 } else {
6169 Self::tie_break_with_line_number(std::iter::empty(), old_start_line, "exact")
6170 };
6171
6172 match result {
6173 Ok(Some(index)) => {
6174 debug!(" Found unique exact match at index {}.", index);
6175 return Ok((
6176 HunkLocation {
6177 start_index: index,
6178 length: match_block.len(),
6179 },
6180 MatchType::Exact,
6181 ));
6182 }
6183 Ok(None) => {} // No exact matches, continue to next strategy.
6184 Err(matches) => return Err(HunkApplyError::AmbiguousExactMatch(matches)),
6185 }
6186 }
6187
6188 // Pre-calculate trimmed lines for subsequent strategies.
6189 // This avoids repeated allocation and trimming in loops.
6190 let target_trimmed: Vec<String> = target_lines
6191 .iter()
6192 .map(|s| s.as_ref().trim_end().to_string())
6193 .collect();
6194 // Create references to the trimmed strings to avoid allocations in TextDiff
6195 let target_refs: Vec<&str> = target_trimmed.iter().map(|s| s.as_str()).collect();
6196
6197 // --- STRATEGY 2: Exact Match (Ignoring Trailing Whitespace) ---
6198 // Handles minor formatting differences.
6199 trace!(" Attempting exact match (ignoring trailing whitespace)...");
6200 {
6201 let match_stripped: Vec<_> = match_block.iter().map(|s| s.trim_end()).collect();
6202 let result = if match_block.len() <= target_lines.len() {
6203 let iter = target_trimmed
6204 .windows(match_block.len())
6205 .enumerate()
6206 .filter(|(_, window)| {
6207 window
6208 .iter()
6209 .map(|s| s.as_str())
6210 .eq(match_stripped.iter().copied())
6211 })
6212 .map(|(i, _)| i);
6213 Self::tie_break_with_line_number(
6214 iter,
6215 old_start_line,
6216 "exact (ignoring whitespace)",
6217 )
6218 } else {
6219 Self::tie_break_with_line_number(
6220 std::iter::empty(),
6221 old_start_line,
6222 "exact (ignoring whitespace)",
6223 )
6224 };
6225
6226 match result {
6227 Ok(Some(index)) => {
6228 debug!(
6229 " Found unique whitespace-insensitive match at index {}.",
6230 index
6231 );
6232 return Ok((
6233 HunkLocation {
6234 start_index: index,
6235 length: match_block.len(),
6236 },
6237 MatchType::ExactIgnoringWhitespace,
6238 ));
6239 }
6240 Ok(None) => {} // No matches, continue.
6241 Err(matches) => return Err(HunkApplyError::AmbiguousExactMatch(matches)),
6242 }
6243 }
6244
6245 // --- STRATEGY 3: Fuzzy Match (with flexible window) ---
6246 // This is the core "smart" logic. If an exact match fails, we search for
6247 // the best-fitting slice in the target file, allowing the slice to be
6248 // slightly larger or smaller than the patch's context. This handles cases
6249 // where lines have been added or removed near the patch location.
6250 if self.options.fuzz_factor > 0.0 && !match_block.is_empty() {
6251 trace!(
6252 " Exact matches failed. Attempting flexible fuzzy match (threshold={:.2})...",
6253 self.options.fuzz_factor
6254 );
6255 if log::log_enabled!(log::Level::Trace) {
6256 trace!(
6257 " Hunk match block ({} lines): {:?}",
6258 match_block.len(),
6259 match_block
6260 );
6261 }
6262
6263 // Hoist invariants for performance
6264 let match_stripped_lines: Vec<&str> =
6265 match_block.iter().map(|s| s.trim_end()).collect();
6266 let match_content = match_stripped_lines.join("\n");
6267
6268 // Pre-calculate trimmed versions for "loose" matching (ignoring indentation)
6269 let match_loose_lines: Vec<&str> = match_block.iter().map(|s| s.trim()).collect();
6270 let match_loose_content = match_loose_lines.join("\n");
6271
6272 let mut best_score = -1.0;
6273 let mut best_ratio_at_best_score = -1.0;
6274 let mut potential_matches = Vec::new(); // Vec<(start_index, length)>
6275
6276 let len = match_block.len();
6277 // Define how far to search for different-sized windows.
6278 // Proportional to hunk size, but with reasonable bounds.
6279 let fuzz_distance = (len / 4).clamp(3, 8);
6280 let min_len = len.saturating_sub(fuzz_distance).max(1);
6281 let max_len = len.saturating_add(fuzz_distance);
6282 trace!(
6283 " Searching with window sizes from {} to {} (hunk size: {}, fuzz distance: {})",
6284 min_len,
6285 max_len,
6286 len,
6287 fuzz_distance
6288 );
6289
6290 // Performance heuristic: narrow down the search space using anchor lines.
6291 let search_ranges = Self::find_search_ranges(match_block, &target_trimmed, len);
6292 trace!(" Using search ranges: {:?}", search_ranges);
6293
6294 // When the anchor heuristic fails, the search can be slow. We parallelize the
6295 // scoring of all possible windows using Rayon if the `parallel` feature is enabled.
6296 #[cfg(feature = "parallel")]
6297 let all_scored_windows: Vec<(f64, f64, f64, f64, usize, usize)> = search_ranges
6298 .par_iter()
6299 .flat_map(|&(range_start, range_end)| {
6300 // By creating local references, we ensure that the inner `move` closures
6301 // capture these references (which are `Copy`) instead of attempting to move
6302 // the original non-`Copy` `Vec` and `String` from the outer scope.
6303 let match_stripped_lines = &match_stripped_lines;
6304 let match_content = &match_content;
6305 let match_loose_lines = &match_loose_lines;
6306 let match_loose_content = &match_loose_content;
6307 let target_slice = &target_refs[range_start..range_end];
6308
6309 (min_len..=max_len)
6310 .into_par_iter()
6311 .filter(move |&window_len| window_len <= target_slice.len())
6312 .flat_map(move |window_len| {
6313 (0..=target_slice.len() - window_len)
6314 .into_par_iter()
6315 .map(move |i| {
6316 let window_stripped_lines = &target_slice[i..i + window_len];
6317 let absolute_index = range_start + i;
6318
6319 // HYBRID SCORING: (Copied from original sequential loop)
6320 let diff_lines = similar::TextDiff::from_slices(
6321 window_stripped_lines,
6322 match_stripped_lines,
6323 );
6324 let ratio_lines = diff_lines.ratio();
6325
6326 let mut capacity = 0;
6327 for line in window_stripped_lines {
6328 capacity += line.len() + 1;
6329 }
6330 let mut window_content = String::with_capacity(capacity);
6331 for (j, line) in window_stripped_lines.iter().enumerate() {
6332 if j > 0 {
6333 window_content.push('\n');
6334 }
6335 window_content.push_str(line);
6336 }
6337
6338 let diff_words = similar::TextDiff::from_words(
6339 &window_content,
6340 match_content,
6341 );
6342 let ratio_words = diff_words.ratio();
6343 // HYBRID SCORING: Give more weight to word-based ratio, as it's
6344 // better at detecting small changes within a line. Line-based
6345 // ratio is still important for overall structure, especially
6346 // when lines are inserted or deleted.
6347 let ratio_strict =
6348 0.3 * ratio_lines as f64 + 0.7 * ratio_words as f64;
6349
6350 // --- LOOSE MATCHING (Ignore Indentation) ---
6351 // Calculate a score based on fully trimmed lines. This helps
6352 // when the patch is nested (e.g. in a markdown list) but the file is flat.
6353 let window_loose_lines: Vec<&str> =
6354 window_stripped_lines.iter().map(|s| s.trim()).collect();
6355 let diff_loose_lines = similar::TextDiff::from_slices(
6356 &window_loose_lines,
6357 match_loose_lines,
6358 );
6359 let ratio_loose_lines = diff_loose_lines.ratio();
6360
6361 let window_loose_content = window_loose_lines.join("\n");
6362 let diff_loose_words = similar::TextDiff::from_words(
6363 &window_loose_content,
6364 match_loose_content,
6365 );
6366 let ratio_loose_words = diff_loose_words.ratio();
6367 let ratio_loose = 0.3 * ratio_loose_lines as f64
6368 + 0.7 * ratio_loose_words as f64;
6369
6370 // The ratio from the `similar` crate already implicitly includes a
6371 // penalty for size differences. We use the raw ratio as the score.
6372 // We take the MAX of strict and loose to support both exact indentation and nested patches.
6373 let ratio = ratio_strict.max(ratio_loose);
6374 let score = ratio;
6375
6376 (
6377 score,
6378 ratio,
6379 ratio_lines as f64,
6380 ratio_words as f64,
6381 absolute_index,
6382 window_len,
6383 )
6384 })
6385 })
6386 })
6387 .collect();
6388
6389 #[cfg(not(feature = "parallel"))]
6390 let all_scored_windows: Vec<(f64, f64, f64, f64, usize, usize)> = search_ranges
6391 .iter()
6392 .flat_map(|&(range_start, range_end)| {
6393 // By creating local references, we ensure that the inner `move` closures
6394 // capture these references (which are `Copy`) instead of attempting to move
6395 // the original non-`Copy` `Vec` and `String` from the outer scope.
6396 let match_stripped_lines = &match_stripped_lines;
6397 let match_content = &match_content;
6398 let match_loose_lines = &match_loose_lines;
6399 let match_loose_content = &match_loose_content;
6400 let target_slice = &target_refs[range_start..range_end];
6401
6402 (min_len..=max_len)
6403 .filter(move |&window_len| window_len <= target_slice.len())
6404 .flat_map(move |window_len| {
6405 (0..=target_slice.len() - window_len).map(move |i| {
6406 let window_stripped_lines = &target_slice[i..i + window_len];
6407 let absolute_index = range_start + i;
6408
6409 // HYBRID SCORING: (Copied from original sequential loop)
6410 let diff_lines = similar::TextDiff::from_slices(
6411 window_stripped_lines,
6412 match_stripped_lines,
6413 );
6414 let ratio_lines = diff_lines.ratio();
6415
6416 let mut capacity = 0;
6417 for line in window_stripped_lines {
6418 capacity += line.len() + 1;
6419 }
6420 let mut window_content = String::with_capacity(capacity);
6421 for (j, line) in window_stripped_lines.iter().enumerate() {
6422 if j > 0 {
6423 window_content.push('\n');
6424 }
6425 window_content.push_str(line);
6426 }
6427
6428 let diff_words =
6429 similar::TextDiff::from_words(&window_content, match_content);
6430 let ratio_words = diff_words.ratio();
6431 // HYBRID SCORING: Give more weight to word-based ratio, as it's
6432 // better at detecting small changes within a line. Line-based
6433 // ratio is still important for overall structure, especially
6434 // when lines are inserted or deleted.
6435 let ratio_strict =
6436 0.3 * ratio_lines as f64 + 0.7 * ratio_words as f64;
6437
6438 // --- LOOSE MATCHING (Ignore Indentation) ---
6439 // Calculate a score based on fully trimmed lines. This helps
6440 // when the patch is nested (e.g. in a markdown list) but the file is flat.
6441 let window_loose_lines: Vec<&str> =
6442 window_stripped_lines.iter().map(|s| s.trim()).collect();
6443 let diff_loose_lines = similar::TextDiff::from_slices(
6444 &window_loose_lines,
6445 match_loose_lines,
6446 );
6447 let ratio_loose_lines = diff_loose_lines.ratio();
6448
6449 let window_loose_content = window_loose_lines.join("\n");
6450 let diff_loose_words = similar::TextDiff::from_words(
6451 &window_loose_content,
6452 match_loose_content,
6453 );
6454 let ratio_loose_words = diff_loose_words.ratio();
6455 let ratio_loose =
6456 0.3 * ratio_loose_lines as f64 + 0.7 * ratio_loose_words as f64;
6457
6458 // The ratio from the `similar` crate already implicitly includes a
6459 // penalty for size differences. We use the raw ratio as the score.
6460 // We take the MAX of strict and loose to support both exact indentation and nested patches.
6461 let ratio = ratio_strict.max(ratio_loose);
6462 let score = ratio;
6463
6464 (
6465 score,
6466 ratio,
6467 ratio_lines as f64,
6468 ratio_words as f64,
6469 absolute_index,
6470 window_len,
6471 )
6472 })
6473 })
6474 })
6475 .collect();
6476
6477 if log::log_enabled!(log::Level::Trace) {
6478 let mut sorted_windows = all_scored_windows.clone();
6479 sorted_windows
6480 .sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
6481 trace!(" Top fuzzy match candidates:");
6482 for (score, ratio, _, _, idx, len) in sorted_windows.iter().take(5) {
6483 let window_content: Vec<_> = target_refs[*idx..*idx + *len].to_vec();
6484 trace!(
6485 " - Index {}, Len {}: Score {:.3} (Ratio {:.3}) | Content: {:?}",
6486 idx,
6487 len,
6488 score,
6489 ratio,
6490 window_content
6491 );
6492 }
6493 }
6494
6495 // Process the collected results sequentially to find the best match and handle tie-breaking.
6496 for (score, ratio, ratio_lines, ratio_words, absolute_index, window_len) in
6497 all_scored_windows
6498 {
6499 // This is the same logic as in the original sequential loop.
6500 if score > best_score {
6501 trace!(
6502 " New best score: {:.3} (ratio {:.3} [l:{:.3},w:{:.3}]) at index {} (window len {})",
6503 score,
6504 ratio,
6505 ratio_lines,
6506 ratio_words,
6507 absolute_index, window_len
6508 );
6509 best_score = score;
6510 best_ratio_at_best_score = ratio;
6511 potential_matches.clear();
6512 potential_matches.push((absolute_index, window_len));
6513 } else if f64::abs(score - best_score) < 1e-9 {
6514 // Tie in score. Prefer the one with the higher raw similarity ratio,
6515 // as it indicates a better content match before size penalties.
6516 if potential_matches.is_empty() {
6517 potential_matches.push((absolute_index, window_len));
6518 continue;
6519 }
6520
6521 if ratio > best_ratio_at_best_score {
6522 // This is a better match despite the same score (e.g., less penalty, more similarity)
6523 trace!(
6524 " Tie in score ({:.3}), but new ratio {:.3} is better than old {:.3}. New best.",
6525 score,
6526 ratio,
6527 best_ratio_at_best_score
6528 );
6529 best_ratio_at_best_score = ratio;
6530 potential_matches.clear();
6531 potential_matches.push((absolute_index, window_len));
6532 } else if f64::abs(ratio - best_ratio_at_best_score) < 1e-9 {
6533 // Also a tie in ratio, so it's a true ambiguity
6534 trace!(
6535 " Tie in score ({:.3}) and ratio ({:.3}). Adding candidate: index {}, len {}",
6536 score,
6537 ratio,
6538 absolute_index,
6539 window_len
6540 );
6541 potential_matches.push((absolute_index, window_len));
6542 }
6543 }
6544 }
6545
6546 trace!(
6547 " Fuzzy search complete. Best score: {:.3}, best ratio: {:.3}, potential matches: {:?}",
6548 best_score,
6549 best_ratio_at_best_score,
6550 potential_matches
6551 );
6552
6553 // Check if the best match found meets the user-defined threshold.
6554 if best_ratio_at_best_score >= f64::from(self.options.fuzz_factor) {
6555 if potential_matches.len() == 1 {
6556 let (start, len) = potential_matches[0];
6557 debug!(
6558 " Found best fuzzy match at index {} (length {}, similarity: {:.3} >= threshold: {:.3}).",
6559 start, len, best_ratio_at_best_score, self.options.fuzz_factor
6560 );
6561 return Ok((
6562 HunkLocation {
6563 start_index: start,
6564 length: len,
6565 },
6566 MatchType::Fuzzy {
6567 score: best_ratio_at_best_score,
6568 },
6569 ));
6570 }
6571 // AMBIGUOUS FUZZY MATCH - TRY TO TIE-BREAK
6572 if let Some(line) = old_start_line {
6573 trace!(
6574 " Ambiguous fuzzy match found at {:?}. Attempting to tie-break using line number hint: {}",
6575 potential_matches,
6576 line
6577 );
6578 let mut closest_match: Option<(usize, usize)> = None;
6579 let mut min_distance = usize::MAX;
6580 let mut is_tie = false;
6581
6582 for &(match_index, match_len) in &potential_matches {
6583 // Hunk line numbers are 1-based, indices are 0-based.
6584 let distance = (match_index + 1).abs_diff(line);
6585 trace!(
6586 " Candidate {:?}: distance from line hint {} is {}",
6587 (match_index, match_len),
6588 line,
6589 distance
6590 );
6591 if distance < min_distance {
6592 min_distance = distance;
6593 closest_match = Some((match_index, match_len));
6594 is_tie = false;
6595 } else if distance == min_distance {
6596 is_tie = true;
6597 }
6598 }
6599
6600 if !is_tie {
6601 if let Some((start, len)) = closest_match {
6602 debug!(
6603 " Tie-broke ambiguous fuzzy match using line number. Best match is at index {} (length {}, similarity: {:.3} >= threshold: {:.3}).",
6604 start, len, best_ratio_at_best_score, self.options.fuzz_factor
6605 );
6606 return Ok((
6607 HunkLocation {
6608 start_index: start,
6609 length: len,
6610 },
6611 MatchType::Fuzzy {
6612 score: best_ratio_at_best_score,
6613 },
6614 ));
6615 }
6616 } else {
6617 trace!(" Tie-breaking failed: multiple fuzzy matches are equidistant from the line number hint.");
6618 }
6619 }
6620 warn!(" Ambiguous fuzzy match: Multiple locations found with same top score ({:.3}): {:?}. Skipping.", best_ratio_at_best_score, potential_matches);
6621 return Err(HunkApplyError::AmbiguousFuzzyMatch(potential_matches));
6622 } else if best_ratio_at_best_score >= 0.0 {
6623 // Did not meet threshold
6624 let (start, len) = potential_matches.first().copied().unwrap_or((0, 0));
6625 debug!(
6626 " Fuzzy match failed: Best location (index {}, len {}) had similarity {:.3}, which is below the threshold of {:.3}.",
6627 start, len, best_ratio_at_best_score, self.options.fuzz_factor
6628 );
6629 return Err(HunkApplyError::FuzzyMatchBelowThreshold {
6630 best_score: best_ratio_at_best_score,
6631 threshold: self.options.fuzz_factor,
6632 location: HunkLocation {
6633 start_index: start,
6634 length: len,
6635 },
6636 });
6637 } else {
6638 // No potential matches found at all
6639 debug!(" Fuzzy match: Could not find any potential match location.");
6640 // Fall through to the final ContextNotFound error
6641 }
6642 } else if self.options.fuzz_factor <= 0.0 {
6643 trace!(" Failed exact matches. Fuzzy matching disabled.");
6644 }
6645
6646 // --- STRATEGY 4: End-of-file fuzzy match for short files ---
6647 // This handles cases where the entire file is a good fuzzy match for the
6648 // start of the hunk context, which can happen if the file is missing
6649 // context lines that the patch expects to be there at the end.
6650 if !target_lines.is_empty()
6651 && target_lines.len() < match_block.len()
6652 && self.options.fuzz_factor > 0.0
6653 {
6654 trace!(" Target file is shorter than hunk. Attempting end-of-file fuzzy match...");
6655 let match_stripped: Vec<&str> = match_block.iter().map(|s| s.trim_end()).collect();
6656 let diff = TextDiff::from_slices(&target_refs, &match_stripped);
6657 let ratio = diff.ratio();
6658
6659 // Be slightly more lenient for this specific end-of-file prefix case.
6660 let effective_threshold = (f64::from(self.options.fuzz_factor) - 0.1).max(0.5);
6661 trace!(
6662 " Using effective threshold for EOF match: {:.3}",
6663 effective_threshold
6664 );
6665
6666 if ratio as f64 >= effective_threshold {
6667 debug!(
6668 " End-of-file fuzzy match succeeded with ratio {:.3} (threshold {:.3}). Treating as full-file match.",
6669 ratio, effective_threshold
6670 );
6671 // We are matching the entire file from the beginning.
6672 return Ok((
6673 HunkLocation {
6674 start_index: 0,
6675 length: target_lines.len(),
6676 },
6677 MatchType::Fuzzy {
6678 score: ratio as f64,
6679 },
6680 ));
6681 } else {
6682 trace!(
6683 " End-of-file fuzzy match ratio {:.3} did not meet effective threshold {:.3}.",
6684 ratio,
6685 effective_threshold
6686 );
6687 }
6688 }
6689
6690 debug!(" Failed to find any suitable match location for hunk.");
6691 Err(HunkApplyError::ContextNotFound)
6692 }
6693
6694 /// Given an iterator of match indices, attempts to find the best one using the
6695 /// hunk's original line number as a hint. Returns the index of the best match,
6696 /// or `None` if the ambiguity cannot be resolved.
6697 /// This function avoids collecting matches into a vector if there are 0 or 1 matches.
6698 fn tie_break_with_line_number(
6699 mut matches: impl Iterator<Item = usize>,
6700 start_line: Option<usize>,
6701 match_type: &str,
6702 ) -> Result<Option<usize>, Vec<usize>> {
6703 // --- Step 1: Check for 0 or 1 matches without allocation ---
6704 let first_match = match matches.next() {
6705 Some(m) => m,
6706 None => {
6707 trace!(" No {} matches found.", match_type);
6708 return Ok(None);
6709 }
6710 };
6711
6712 if let Some(second_match) = matches.next() {
6713 // --- Step 2: Multiple matches found, collect and tie-break ---
6714 // At least two matches exist. Collect them all for analysis.
6715 let mut all_matches = vec![first_match, second_match];
6716 all_matches.extend(matches);
6717
6718 trace!(
6719 " Found {} {} match candidate(s) at indices: {:?}",
6720 all_matches.len(),
6721 match_type,
6722 all_matches
6723 );
6724
6725 // More than 1 match, try to tie-break using the line number hint.
6726 if let Some(line) = start_line {
6727 trace!(
6728 " Ambiguous {} match found at {:?}. Attempting to tie-break using line number hint: {}",
6729 match_type,
6730 all_matches,
6731 line
6732 );
6733 let mut closest_index = 0;
6734 let mut min_distance = usize::MAX;
6735 let mut is_tie = false;
6736
6737 // Find the match that is numerically closest to the hint.
6738 for &match_index in &all_matches {
6739 // Hunk line numbers are 1-based, indices are 0-based.
6740 let distance = (match_index + 1).abs_diff(line);
6741 trace!(
6742 " Candidate index {}: distance from line hint {} is {}",
6743 match_index,
6744 line,
6745 distance
6746 );
6747 if distance < min_distance {
6748 min_distance = distance;
6749 closest_index = match_index;
6750 is_tie = false;
6751 } else if distance == min_distance {
6752 // If another match has the same minimum distance, it's a tie.
6753 is_tie = true;
6754 }
6755 }
6756
6757 if !is_tie {
6758 trace!(
6759 " Successfully tie-broke using line number. Best match is at index {}.",
6760 closest_index
6761 );
6762 return Ok(Some(closest_index));
6763 }
6764 trace!(
6765 " Tie-breaking failed: multiple matches are equidistant from the line number hint."
6766 );
6767 } else {
6768 trace!(
6769 " tie_break: Ambiguous '{}' match, but no line number hint provided.",
6770 match_type
6771 );
6772 }
6773
6774 // If we reach here, the ambiguity could not be resolved.
6775 Err(all_matches)
6776 } else {
6777 // Exactly one match was found.
6778 trace!(
6779 " Found 1 {} match candidate at index: {}",
6780 match_type,
6781 first_match
6782 );
6783 trace!(
6784 " tie_break: Only one match found for '{}' match at index {}. No tie-break needed.",
6785 match_type,
6786 first_match
6787 );
6788 Ok(Some(first_match))
6789 }
6790 }
6791}
6792
6793impl<'a> HunkFinder for DefaultHunkFinder<'a> {
6794 /// Finds the location to apply a hunk to a slice of lines.
6795 ///
6796 /// This implementation uses a hierarchical approach: exact match, exact match
6797 /// ignoring trailing whitespace, and finally a flexible fuzzy match.
6798 ///
6799 /// # Arguments
6800 ///
6801 /// * `hunk` - The [`Hunk`] to locate.
6802 /// * `target_lines` - A slice of strings representing the content to search within.
6803 ///
6804 /// # Returns
6805 ///
6806 /// A tuple containing the [`HunkLocation`] and the [`MatchType`] on success.
6807 ///
6808 /// # Errors
6809 ///
6810 /// Returns `Err(`[`HunkApplyError`]`)` if no suitable location could be found.
6811 ///
6812 /// # Examples
6813 ///
6814 /// ```
6815 /// # use mpatch::{parse_single_patch, DefaultHunkFinder, HunkFinder, ApplyOptions, HunkLocation, MatchType};
6816 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
6817 /// let diff = "```diff\n--- a/f\n+++ b/f\n@@ -1,2 +1,2\n line 1\n-line 2\n+line two\n```";
6818 /// let hunk = parse_single_patch(diff)?.hunks.remove(0);
6819 /// let target_lines = vec!["line 1", "line 2"];
6820 /// let options = ApplyOptions::new();
6821 /// let finder = DefaultHunkFinder::new(&options);
6822 /// let (location, match_type) = finder.find_location(&hunk, &target_lines)?;
6823 /// assert_eq!(location, HunkLocation { start_index: 0, length: 2 });
6824 /// assert!(matches!(match_type, MatchType::Exact));
6825 /// # Ok(())
6826 /// # }
6827 /// ```
6828 fn find_location<T: AsRef<str> + Sync>(
6829 &self,
6830 hunk: &Hunk,
6831 target_lines: &[T],
6832 ) -> Result<(HunkLocation, MatchType), HunkApplyError> {
6833 let match_block = hunk.get_match_block();
6834 self.find_hunk_location_internal(&match_block, target_lines, hunk.old_start_line)
6835 }
6836}
6837
6838/// Finds the location to apply a hunk to a given text content without modifying it.
6839///
6840/// This function encapsulates the core context-aware search logic of `mpatch`. It
6841/// performs a series of checks, from exact matching to fuzzy matching, to determine
6842/// the optimal position to apply the hunk. It is a read-only operation.
6843///
6844/// This is useful for tools that want to analyze where a patch would apply without
6845/// actually performing the patch, or for building custom patch application logic.
6846///
6847/// # Arguments
6848///
6849/// **Note:** For improved performance when content is already available as a slice
6850/// of lines, consider using [`find_hunk_location_in_lines()`].
6851///
6852/// * `hunk` - A reference to the [`Hunk`] to be located.
6853/// * `target_content` - A string slice of the content to search within.
6854/// * `options` - Configuration for the patch operation, such as `fuzz_factor`.
6855///
6856/// # Returns
6857///
6858/// A tuple containing the [`HunkLocation`] and the [`MatchType`] on success.
6859///
6860/// # Errors
6861///
6862/// Returns `Err(`[`HunkApplyError`]`)` if no suitable location could be found, with a reason
6863/// for the failure (e.g., context not found, ambiguous match).
6864///
6865/// # Examples
6866///
6867/// ````rust
6868/// # use mpatch::{parse_single_patch, find_hunk_location, HunkLocation, ApplyOptions, MatchType};
6869/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
6870/// let original_content = "line 1\nline two\nline 3\n";
6871/// let diff_content = r#"
6872/// ```diff
6873/// --- a/file.txt
6874/// +++ b/file.txt
6875/// @@ -1,3 +1,3 @@
6876/// line 1
6877/// -line two
6878/// +line 2
6879/// line 3
6880/// ```
6881/// "#;
6882///
6883/// let patch = parse_single_patch(diff_content)?;
6884/// let hunk = &patch.hunks[0];
6885///
6886/// let options = ApplyOptions::exact();
6887/// let (location, match_type) = find_hunk_location(hunk, original_content, &options)?;
6888///
6889/// assert_eq!(location, HunkLocation { start_index: 0, length: 3 });
6890/// assert!(matches!(match_type, MatchType::Exact));
6891/// # Ok(())
6892/// # }
6893/// ````
6894pub fn find_hunk_location(
6895 hunk: &Hunk,
6896 target_content: &str,
6897 options: &ApplyOptions,
6898) -> Result<(HunkLocation, MatchType), HunkApplyError> {
6899 let target_lines: Vec<_> = target_content.lines().collect();
6900 find_hunk_location_in_lines(hunk, &target_lines, options)
6901}
6902
6903/// Finds the location to apply a hunk to a slice of lines without modifying it.
6904///
6905/// This is a more allocation-friendly version of [`find_hunk_location()`] that
6906/// operates directly on a slice of strings, avoiding the need to join and re-split
6907/// content. This is useful for tools that already have content in a line-based
6908/// format.
6909///
6910/// # Arguments
6911///
6912/// * `hunk` - A reference to the [`Hunk`] to be located.
6913/// * `target_lines` - A slice of strings representing the content to search within.
6914/// The slice can contain `String` or `&str`.
6915/// * `options` - Configuration for the patch operation, such as `fuzz_factor`.
6916///
6917/// # Returns
6918///
6919/// A tuple containing the [`HunkLocation`] and the [`MatchType`] on success.
6920///
6921/// # Errors
6922///
6923/// Returns `Err(`[`HunkApplyError`]`)` if no suitable location could be found.
6924///
6925/// # Examples
6926///
6927/// ````rust
6928/// # use mpatch::{parse_single_patch, find_hunk_location_in_lines, HunkLocation, ApplyOptions, MatchType};
6929/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
6930/// let original_lines = vec!["line 1", "line two", "line 3"];
6931/// let diff_content = r#"
6932/// ```diff
6933/// --- a/file.txt
6934/// +++ b/file.txt
6935/// @@ -1,3 +1,3 @@
6936/// line 1
6937/// -line two
6938/// +line 2
6939/// line 3
6940/// ```
6941/// "#;
6942///
6943/// let patch = parse_single_patch(diff_content)?;
6944/// let hunk = &patch.hunks[0];
6945///
6946/// let options = ApplyOptions::exact();
6947/// let (location, match_type) = find_hunk_location_in_lines(hunk, &original_lines, &options)?;
6948///
6949/// assert_eq!(location, HunkLocation { start_index: 0, length: 3 });
6950/// assert!(matches!(match_type, MatchType::Exact));
6951/// # Ok(())
6952/// # }
6953/// ````
6954pub fn find_hunk_location_in_lines<T: AsRef<str> + Sync>(
6955 hunk: &Hunk,
6956 target_lines: &[T],
6957 options: &ApplyOptions,
6958) -> Result<(HunkLocation, MatchType), HunkApplyError> {
6959 let finder = DefaultHunkFinder::new(options);
6960 finder.find_location(hunk, target_lines)
6961}
6962
6963/// Parses a hunk header line (e.g., "@@ -1,3 +1,3 @@") to extract the starting line number.
6964fn parse_hunk_header(line: &str) -> (Option<usize>, Option<usize>) {
6965 // We are interested in the original file's line number, which is the first number after '-'.
6966 // Example: @@ -21,8 +21,8 @@
6967 let parts: Vec<_> = line.split_whitespace().collect();
6968 if parts.len() < 3 {
6969 return (None, None);
6970 }
6971 let old_line = parts[1]
6972 .strip_prefix('-')
6973 .and_then(|s| s.split(',').next())
6974 .and_then(|s| s.parse::<usize>().ok());
6975 let new_line = parts[2]
6976 .strip_prefix('+')
6977 .and_then(|s| s.split(',').next())
6978 .and_then(|s| s.parse::<usize>().ok());
6979 (old_line, new_line)
6980}