diff --git a/src/lib.rs b/src/lib.rs index e1a9a2e..f96b6f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! ## Quick Start //! //! A simple Vec of primitives can be diffed using Myers algorithm. -//! The diff can be transformed into a series of [`Hunk`]s. +//! The diff can be transformed into a series of [`patch::Hunk`]s. //! Hunks can be transformed into a textual diff or applied to an input. //! //! `apply(&old, hunks(diff(&old, &new))) == Ok(new)` @@ -33,7 +33,7 @@ //! ``` //! //! For nested structures a recursive diffing algorithm is provided. -//! The diff will return a list of [`Change`]s. +//! The diff will return a list of [`recursive::Change`]s. //! Changes can be transformed into Hunks and applied. //! Changes cannot be serialized, since there is no consensus on a textual format. //! diff --git a/src/myers/mod.rs b/src/myers/mod.rs index 6052957..fc87a85 100644 --- a/src/myers/mod.rs +++ b/src/myers/mod.rs @@ -29,8 +29,8 @@ impl V { /// Computes the diff between two strings after breaking them into newlines /// and running `diff`. pub fn diff_lines(old: &str, new: &str) -> Diff { - let old_lines: Vec = old.split('\n').map(|l| l.to_string()).collect(); - let new_lines: Vec = new.split('\n').map(|l| l.to_string()).collect(); + let old_lines: Vec = old.split('\n').map(ToString::to_string).collect(); + let new_lines: Vec = new.split('\n').map(ToString::to_string).collect(); diff(&old_lines, &new_lines) } @@ -125,9 +125,9 @@ fn traceback( } if d > 0 { if prev_k == k - 1 { - changes.push(Edit::Delete(old[x - 1].clone())) + changes.push(Edit::Delete(old[x - 1].clone())); } else { - changes.push(Edit::Insert(new[y - 1].clone())) + changes.push(Edit::Insert(new[y - 1].clone())); } } x = prev_x; diff --git a/src/patch/mod.rs b/src/patch/mod.rs index eb8b7a8..7731ea6 100644 --- a/src/patch/mod.rs +++ b/src/patch/mod.rs @@ -62,7 +62,7 @@ impl HunkBuilder { old_start, new_start, changes, - }); + }) }; match modify { @@ -111,7 +111,12 @@ pub fn hunks(edits: Vec>) -> Vec> { } /// Applies a list of hunks to an input -/// Can return a PatchError in case of mismatches between hunks and input +/// Can return a [`PatchError`] in case of mismatches between hunks and input. +/// +/// # Errors +/// +/// Returns [`PatchError::InvalidFormat`] if a hunk's context lines don't match +/// the corresponding lines in `old`, or if hunks cannot be applied in order. /// ``` /// use patchwork::myers::{diff, Edit}; /// use patchwork::patch::{apply, Hunk}; @@ -154,33 +159,37 @@ pub fn apply( while old_line < old.len() { if let Some(hunk) = hunk_iter.peek() { - if old_line == hunk.old_start { - for change in &hunk.changes { - match change { - Edit::Equal(t) => { - if old[old_line] != *t { - return Err(PatchError::InvalidFormat(format!( - "Context mismatch at line {}: expected '{}', found '{}'", - old_line, t, old[old_line] - ))); + match old_line.cmp(&hunk.old_start) { + std::cmp::Ordering::Equal => { + for change in &hunk.changes { + match change { + Edit::Equal(t) => { + if old[old_line] != *t { + return Err(PatchError::InvalidFormat(format!( + "Context mismatch at line {}: expected '{}', found '{}'", + old_line, t, old[old_line] + ))); + } + result.push(old[old_line].clone()); + old_line += 1; + } + Edit::Insert(t) => { + result.push(t.clone()); + } + Edit::Delete(_) => { + old_line += 1; } - result.push(old[old_line].clone()); - old_line += 1; - } - Edit::Insert(t) => { - result.push(t.clone()); - } - Edit::Delete(_) => { - old_line += 1; } } + hunk_iter.next(); + } + std::cmp::Ordering::Less => { + result.push(old[old_line].clone()); + old_line += 1; + } + std::cmp::Ordering::Greater => { + return Err(PatchError::InvalidFormat("Cannot apply hunks".to_string())); } - hunk_iter.next(); - } else if old_line < hunk.old_start { - result.push(old[old_line].clone()); - old_line += 1; - } else { - return Err(PatchError::InvalidFormat("Cannot apply hunks".to_string())); } } else { result.push(old[old_line].clone()); diff --git a/src/patch/types.rs b/src/patch/types.rs index 222fb90..ebbb70d 100644 --- a/src/patch/types.rs +++ b/src/patch/types.rs @@ -1,7 +1,7 @@ use crate::myers::Edit; /// Represents a Hunk resulting from a Myers diff. -/// Please note that `changes` will include maximum 3 context elements, i.e. Edit::Equal +/// Please note that `changes` will include maximum 3 context elements, i.e. `Edit::Equal` /// and this is reflected in the `old_start` value #[derive(Debug, Clone, PartialEq, Eq)] pub struct Hunk { diff --git a/src/recursive/diffable.rs b/src/recursive/diffable.rs index 3892382..0fee35e 100644 --- a/src/recursive/diffable.rs +++ b/src/recursive/diffable.rs @@ -1,4 +1,4 @@ -use crate::recursive::types::*; +use crate::recursive::types::{Node, Primitive}; use std::collections::HashMap; /// Trait to transform a given structure into a `[Node]` tree or viceversa. @@ -18,12 +18,12 @@ pub trait Diffable { impl Diffable for Vec { type P = T::P; fn to_node(&self) -> Node { - Node::Sequence(self.iter().map(|e| e.to_node()).collect()) + Node::Sequence(self.iter().map(Diffable::to_node).collect()) } fn from_node(node: Node) -> Self { match node { - Node::Sequence(v) => v.into_iter().map(|e| T::from_node(e)).collect(), + Node::Sequence(v) => v.into_iter().map(T::from_node).collect(), _ => unreachable!(), } } diff --git a/src/serialization.rs b/src/serialization.rs index 2d50fb1..8ce151e 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -18,14 +18,20 @@ pub trait ToPatch: Sized { /// /// Implemented for `Edit` and `Vec>`. pub trait FromPatch: Sized { + /// Parse a unified diff patch string into a structured representation. + /// + /// # Errors + /// + /// Returns [`PatchError::InvalidFormat`] if the patch header is missing or malformed. + /// Returns [`PatchError::UnexpectedToken`] if a line starts with an unexpected character. fn from_patch(s: &str) -> Result; } -/// Represents an error parsing or applying a diff +/// Represents an error parsing or applying a diff. #[derive(Debug, PartialEq)] pub enum PatchError { - /// The patch is structurally invalid, e.g. missing `---`/`+++` header. - /// The patch cannot be applied to the given structure. + /// The patch is structurally invalid, e.g. missing `---`/`+++` header, + /// or the patch cannot be applied to the given structure. InvalidFormat(String), /// A line in the patch starts with an unexpected character. UnexpectedToken(String), @@ -92,7 +98,8 @@ impl ToPatch for Vec> { let hunks = self .iter() .map(|h| h.to_patch(None, None)) - .collect::(); + .collect::>() + .join("\n"); format!("{}{}", header, hunks) } } @@ -187,4 +194,42 @@ mod tests { prop_assert_eq!(Vec::>::from_patch(&patch).unwrap(), hunks); } } + + #[test] + fn test_multi_hunk_patch_format() { + let old: Vec<&str> = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; + let new: Vec<&str> = vec!["X", "b", "c", "d", "e", "f", "g", "h", "i", "Y"]; + let edits = diff(&old, &new); + let h = hunks(edits); + assert_eq!(h.len(), 2, "expected 2 hunks"); + let patch = h.to_patch(Some("old.txt"), Some("new.txt")); + // Each @@ header must start on its own line + for line in patch.lines() { + if line.starts_with("@@") || line.starts_with("---") || line.starts_with("+++") { + continue; + } + assert!( + !line.contains("@@"), + "@@ header is not on its own line: {:?}", + line + ); + } + } + + #[test] + fn test_multi_hunk_roundtrip() { + let old: Vec = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] + .into_iter() + .map(String::from) + .collect(); + let new: Vec = vec!["X", "b", "c", "d", "e", "f", "g", "h", "i", "Y"] + .into_iter() + .map(String::from) + .collect(); + let edits = diff(&old, &new); + let h = hunks(edits); + let patch = h.to_patch(Some("old.txt"), Some("new.txt")); + let parsed = Vec::>::from_patch(&patch).unwrap(); + assert_eq!(parsed, h); + } }