use std::borrow::Cow;
use std::fmt;
use chrono::{DateTime, FixedOffset};
use crate::parser::{parse_multiple_patches, parse_single_patch, ParseError};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Patch<'a> {
pub old: File<'a>,
pub new: File<'a>,
pub hunks: Vec<Hunk<'a>>,
pub end_newline: bool,
}
impl<'a> fmt::Display for Patch<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "--- {}", self.old)?;
write!(f, "\n+++ {}", self.new)?;
for hunk in &self.hunks {
write!(f, "\n{}", hunk)?;
}
if !self.end_newline {
write!(f, "\n\\ No newline at end of file")?;
}
Ok(())
}
}
impl<'a> Patch<'a> {
#[allow(clippy::tabs_in_doc_comments)]
pub fn from_single(s: &'a str) -> Result<Self, ParseError<'a>> {
parse_single_patch(s)
}
pub fn from_multiple(s: &'a str) -> Result<Vec<Self>, ParseError<'a>> {
parse_multiple_patches(s)
}
}
fn maybe_escape_quote(f: &mut fmt::Formatter, s: &str) -> fmt::Result {
let quote = s
.chars()
.any(|ch| matches!(ch, ' ' | '\t' | '\r' | '\n' | '\"' | '\0' | '\\'));
if quote {
write!(f, "\"")?;
for ch in s.chars() {
match ch {
'\0' => write!(f, r"\0")?,
'\n' => write!(f, r"\n")?,
'\r' => write!(f, r"\r")?,
'\t' => write!(f, r"\t")?,
'"' => write!(f, r#"\""#)?,
'\\' => write!(f, r"\\")?,
_ => write!(f, "{}", ch)?,
}
}
write!(f, "\"")
} else {
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct File<'a> {
pub path: Cow<'a, str>,
pub meta: Option<FileMetadata<'a>>,
}
impl<'a> fmt::Display for File<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
maybe_escape_quote(f, &self.path)?;
if let Some(meta) = &self.meta {
write!(f, "\t{}", meta)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum FileMetadata<'a> {
DateTime(DateTime<FixedOffset>),
Other(Cow<'a, str>),
}
impl<'a> fmt::Display for FileMetadata<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FileMetadata::DateTime(datetime) => {
write!(f, "{}", datetime.format("%F %T%.f %z"))
}
FileMetadata::Other(data) => maybe_escape_quote(f, data),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Hunk<'a> {
pub old_range: Range,
pub new_range: Range,
pub range_hint: &'a str,
pub lines: Vec<Line<'a>>,
}
impl<'a> Hunk<'a> {
pub fn hint(&self) -> Option<&str> {
let h = self.range_hint.trim_start();
if h.is_empty() {
None
} else {
Some(h)
}
}
}
impl<'a> fmt::Display for Hunk<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"@@ -{} +{} @@{}",
self.old_range, self.new_range, self.range_hint
)?;
for line in &self.lines {
write!(f, "\n{}", line)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Range {
pub start: u64,
pub count: u64,
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{},{}", self.start, self.count)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Line<'a> {
Add(&'a str),
Remove(&'a str),
Context(&'a str),
}
impl<'a> fmt::Display for Line<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Line::Add(line) => write!(f, "+{}", line),
Line::Remove(line) => write!(f, "-{}", line),
Line::Context(line) => write!(f, " {}", line),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_hint_helper() {
let mut h = Hunk {
old_range: Range { start: 0, count: 0 },
new_range: Range { start: 0, count: 0 },
range_hint: "",
lines: vec![],
};
for (input, expected) in vec![
("", None),
(" ", None),
(" ", None),
("x", Some("x")),
(" x", Some("x")),
("x ", Some("x ")),
(" x ", Some("x ")),
(" abc def ", Some("abc def ")),
] {
h.range_hint = input;
assert_eq!(h.hint(), expected);
}
}
}