Skip to main content

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) = &current_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}