mod format;
mod parse;
pub use format::PatchFormatter;
pub use parse::{HunkRangeStrategy, ParsePatchError, ParserConfig};
use std::{
borrow::Cow,
fmt::{self, Debug},
ops,
};
use crate::{LineEnd, utils::Text};
const NO_NEWLINE_AT_EOF: &str = "\\ No newline at end of file";
pub type Patch<'a, T> = Vec<Diff<'a, T>>;
#[derive(Clone, PartialEq, PartialOrd, Ord, Eq)]
pub struct Diff<'a, T: ToOwned + ?Sized> {
original: Option<Filename<'a, T>>,
modified: Option<Filename<'a, T>>,
hunks: Vec<Hunk<'a, T>>,
}
impl<'a, T: Text + ToOwned + ?Sized> Diff<'a, T> {
pub(crate) fn new<O, M>(
original: Option<O>,
modified: Option<M>,
hunks: Vec<Hunk<'a, T>>,
) -> Self
where
O: Into<Cow<'a, T>>,
M: Into<Cow<'a, T>>,
{
let original = original.map(|o| Filename(o.into()));
let modified = modified.map(|m| Filename(m.into()));
Self {
original,
modified,
hunks,
}
}
pub fn original(&self) -> Option<&T> {
self.original.as_ref().map(AsRef::as_ref)
}
pub fn modified(&self) -> Option<&T> {
self.modified.as_ref().map(AsRef::as_ref)
}
pub fn hunks(&self) -> &[Hunk<'_, T>] {
&self.hunks
}
pub fn reverse(&self) -> Diff<'_, T> {
let hunks = self.hunks.iter().map(Hunk::reverse).collect();
Diff {
original: self.modified.clone(),
modified: self.original.clone(),
hunks,
}
}
}
impl<T: AsRef<[u8]> + ToOwned + ?Sized> Diff<'_, T> {
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
PatchFormatter::new()
.write_patch_into(self, &mut bytes)
.unwrap();
bytes
}
}
pub fn patch_from_str(input: &str) -> Result<Patch<'_, str>, ParsePatchError> {
parse::parse_multiple(input)
}
pub fn patch_from_str_with_config(
input: &str,
config: ParserConfig,
) -> Result<Patch<'_, str>, ParsePatchError> {
parse::parse_multiple_with_config(input, config)
}
pub fn patch_from_bytes(input: &[u8]) -> Result<Patch<'_, [u8]>, ParsePatchError> {
parse::parse_bytes_multiple(input)
}
pub fn patch_from_bytes_with_config(
input: &[u8],
config: ParserConfig,
) -> Result<Patch<'_, [u8]>, ParsePatchError> {
parse::parse_bytes_multiple_with_config(input, config)
}
impl<'a> Diff<'a, str> {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &'a str) -> Result<Diff<'a, str>, ParsePatchError> {
parse::parse(s)
}
}
impl<'a> Diff<'a, [u8]> {
pub fn from_bytes(s: &'a [u8]) -> Result<Diff<'a, [u8]>, ParsePatchError> {
parse::parse_bytes(s)
}
}
impl fmt::Display for Diff<'_, str> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", PatchFormatter::new().fmt_patch(self))
}
}
impl<T> fmt::Debug for Diff<'_, T>
where
T: ?Sized + ToOwned<Owned: Debug> + fmt::Debug + Text,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Patch")
.field("original", &self.original)
.field("modified", &self.modified)
.field("hunks", &self.hunks)
.finish()
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Filename<'a, T: ToOwned + ?Sized>(Cow<'a, T>);
const ESCAPED_CHARS: &[char] = &['\n', '\t', '\0', '\r', '\"', '\\'];
#[allow(clippy::byte_char_slices)]
const ESCAPED_CHARS_BYTES: &[u8] = &[b'\n', b'\t', b'\0', b'\r', b'\"', b'\\'];
impl Filename<'_, str> {
fn needs_to_be_escaped(&self) -> bool {
self.0.contains(ESCAPED_CHARS)
}
}
impl<T: ToOwned + AsRef<[u8]> + ?Sized> Filename<'_, T> {
fn needs_to_be_escaped_bytes(&self) -> bool {
self.0
.as_ref()
.as_ref()
.iter()
.any(|b| ESCAPED_CHARS_BYTES.contains(b))
}
fn write_into<W: std::io::Write>(&self, mut w: W) -> std::io::Result<()> {
if self.needs_to_be_escaped_bytes() {
w.write_all(b"\"")?;
for b in self.0.as_ref().as_ref() {
if ESCAPED_CHARS_BYTES.contains(b) {
w.write_all(b"\\")?;
}
w.write_all(&[*b])?;
}
w.write_all(b"\"")?;
} else {
w.write_all(self.0.as_ref().as_ref())?;
}
Ok(())
}
}
impl<T: ToOwned + ?Sized> AsRef<T> for Filename<'_, T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T: ToOwned + ?Sized> ops::Deref for Filename<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: ToOwned + ?Sized> Clone for Filename<'_, T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl fmt::Display for Filename<'_, str> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write;
if self.needs_to_be_escaped() {
f.write_char('\"')?;
for c in self.0.chars() {
if ESCAPED_CHARS.contains(&c) {
f.write_char('\\')?;
}
f.write_char(c)?;
}
f.write_char('\"')?;
} else {
f.write_str(&self.0)?;
}
Ok(())
}
}
impl<T> fmt::Debug for Filename<'_, T>
where
T: Debug + ToOwned<Owned: Debug> + ?Sized + Text,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let internal_value = self.0.as_ref();
let probably_readable = internal_value
.as_str()
.map(String::from)
.unwrap_or_else(|| format!("{:#?}", internal_value));
f.debug_tuple("Filename").field(&probably_readable).finish()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Hunk<'a, T: ?Sized + ToOwned> {
old_range: HunkRange,
new_range: HunkRange,
function_context: Option<(&'a T, Option<LineEnd>)>,
lines: Vec<Line<'a, T>>,
}
impl fmt::Display for Hunk<'_, str> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
PatchFormatter::new().fmt_hunk(self).fmt(f)
}
}
impl fmt::Display for Hunk<'_, [u8]> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buf = Vec::new();
PatchFormatter::new()
.write_hunk_into(self, &mut buf)
.map_err(|_| fmt::Error)?;
let s = String::from_utf8_lossy(&buf);
write!(f, "{}", s)
}
}
impl<T> Debug for Hunk<'_, T>
where
T: Debug + ToOwned + ?Sized + Text,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut fmt = f.debug_struct("Hunk");
fmt.field("old_range", &self.old_range);
fmt.field("new_range", &self.new_range);
if let Some((line, ending)) = self.function_context {
let mut l = line.as_bytes().to_owned();
let e: &[u8] = ending.map(|e| e.into()).unwrap_or(&[]);
l.extend(e);
if let Ok(s) = std::str::from_utf8(l.as_slice()) {
fmt.field("function_context", &Some(s));
} else {
fmt.field("function_context", &Some(l));
}
} else {
fmt.field("function_context", &None::<&str>);
}
fmt.field("lines", &self.lines).finish()
}
}
fn hunk_lines_count<T: ?Sized + ToOwned>(lines: &[Line<'_, T>]) -> (usize, usize) {
lines.iter().fold((0, 0), |count, line| match line {
Line::Context(_) => (count.0 + 1, count.1 + 1),
Line::Delete(_) => (count.0 + 1, count.1),
Line::Insert(_) => (count.0, count.1 + 1),
})
}
impl<'a, T: Text + ?Sized + ToOwned> Hunk<'a, T> {
pub(crate) fn new(
old_range: HunkRange,
new_range: HunkRange,
function_context: Option<(&'a T, Option<LineEnd>)>,
lines: Vec<Line<'a, T>>,
) -> Self {
Self {
old_range,
new_range,
function_context,
lines,
}
}
pub fn old_range(&self) -> HunkRange {
self.old_range
}
pub fn new_range(&self) -> HunkRange {
self.new_range
}
pub fn function_context(&self) -> Option<(&T, Option<LineEnd>)> {
self.function_context
}
pub fn lines(&self) -> &[Line<'a, T>] {
&self.lines
}
pub fn reverse(&self) -> Self {
let lines = self.lines.iter().map(Line::reverse).collect();
Self {
old_range: self.new_range,
new_range: self.old_range,
function_context: self.function_context,
lines,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HunkRange {
start: usize,
len: usize,
}
impl HunkRange {
pub(crate) fn new(start: usize, len: usize) -> Self {
Self { start, len }
}
pub fn range(&self) -> ops::Range<usize> {
self.start..self.end()
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.start + self.len
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
impl fmt::Display for HunkRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.start)?;
if self.len != 1 {
write!(f, ",{}", self.len)?;
}
Ok(())
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Line<'a, T: ?Sized> {
Context((&'a T, Option<LineEnd>)),
Delete((&'a T, Option<LineEnd>)),
Insert((&'a T, Option<LineEnd>)),
}
impl<T: ?Sized + Text> fmt::Debug for Line<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Line::Context((line, ending)) => {
let mut l = line.as_bytes().to_owned();
let e: &[u8] = ending.map(|e| e.into()).unwrap_or(&[]);
l.extend(e);
if let Ok(s) = std::str::from_utf8(l.as_slice()) {
f.debug_tuple("Context").field(&s).finish()
} else {
f.debug_tuple("Context").field(&l).finish()
}
}
Line::Delete((line, ending)) => {
let mut l = line.as_bytes().to_owned();
let e: &[u8] = ending.map(|e| e.into()).unwrap_or(&[]);
l.extend(e);
if let Ok(s) = std::str::from_utf8(l.as_slice()) {
f.debug_tuple("Delete").field(&s).finish()
} else {
f.debug_tuple("Delete").field(&l).finish()
}
}
Line::Insert((line, ending)) => {
let mut l = line.as_bytes().to_owned();
let e: &[u8] = ending.map(|e| e.into()).unwrap_or(&[]);
l.extend(e);
if let Ok(s) = std::str::from_utf8(l.as_slice()) {
f.debug_tuple("Insert").field(&s).finish()
} else {
f.debug_tuple("Insert").field(&l).finish()
}
}
}
}
}
impl<T: ?Sized> Line<'_, T> {
pub fn reverse(&self) -> Self {
match *self {
Line::Context(l) => Line::Context(l),
Line::Delete(l) => Line::Insert(l),
Line::Insert(l) => Line::Delete(l),
}
}
pub fn line_end(&self) -> Option<LineEnd> {
match self {
Line::Context(l) => l.1,
Line::Delete(l) => l.1,
Line::Insert(l) => l.1,
}
}
}