#![allow(clippy::no_effect)] use core::fmt; use std::{ fs, path::{Path, PathBuf}, }; use anyhow::{Result, bail}; use pretty_assertions::assert_eq; #[derive(Clone)] pub(crate) struct Patch { pub(crate) files: Vec, pub(crate) commit_message: String, pub(crate) date: String, pub(crate) final_whitespace: &'static str, pub(crate) generating_version: String, } impl fmt::Display for Patch { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for file in &self.files { write!(f, "{}", file)?; } writeln!(f, "--{}", self.final_whitespace)?; writeln!(f, "{}", self.generating_version)?; Ok(()) } } #[derive(Clone)] pub(crate) enum FileDiff { Add { path: PathBuf, diff: String, index: String, new_file_mode: String, }, Remove { path: PathBuf, diff: String, index: String, deleted_mode: String, }, Change { path: PathBuf, diff: String, index: String, }, RenameNoChange { from: PathBuf, to: PathBuf, }, RenameWithChange { from: PathBuf, to: PathBuf, similarity: String, diff: String, index: String, }, } impl fmt::Display for FileDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FileDiff::Add { path, diff, index, new_file_mode, } => { writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?; writeln!(f, "new file mode {}", new_file_mode)?; writeln!(f, "index {}", index)?; writeln!(f, "--- /dev/null")?; writeln!(f, "+++ b/{}", path.display())?; writeln!(f, "{}", diff)?; } FileDiff::Remove { path, diff, index, deleted_mode, } => { writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?; writeln!(f, "deleted file mode {}", deleted_mode)?; writeln!(f, "index {}", index)?; writeln!(f, "--- a/{}", path.display())?; writeln!(f, "+++ /dev/null")?; writeln!(f, "{}", diff)?; } FileDiff::Change { path, diff, index } => { writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?; writeln!(f, "index {}", index)?; writeln!(f, "--- a/{}", path.display())?; writeln!(f, "+++ b/{}", path.display())?; writeln!(f, "{}", diff)?; } FileDiff::RenameNoChange { from, to } => { writeln!(f, "diff --git a/{} b/{}", from.display(), to.display())?; writeln!(f, "similarity index 100%")?; writeln!(f, "rename from {}", from.display())?; writeln!(f, "rename to {}", to.display())?; } FileDiff::RenameWithChange { from, to, similarity, diff, index, } => { writeln!(f, "diff --git a/{} b/{}", from.display(), to.display())?; writeln!(f, "similarity index {}", similarity)?; writeln!(f, "rename from {}", from.display())?; writeln!(f, "rename to {}", to.display())?; writeln!(f, "index {}", index)?; writeln!(f, "--- a/{}", from.display())?; writeln!(f, "+++ b/{}", to.display())?; writeln!(f, "{}", diff)?; } } Ok(()) } } macro_rules! expect { (if $gate:ident -> $prefix:literal from $line:expr; assign |$strp_line:ident| $callback:block) => { expect!(@base continue; if $gate -> $prefix from $line; assign |$strp_line| $callback) }; (@no_wrap if $gate:ident -> $prefix:literal from $line:expr; assign |$strp_line:ident| $callback:block) => { expect!(@base (); if $gate -> $prefix from $line; assign |$strp_line| $callback) }; (@base $continue:expr; if $gate:ident -> $prefix:literal from $line:expr; assign |$strp_line:ident| $callback:block) => { if $gate.is_none() { let $strp_line: &str = $line .strip_prefix($prefix) .expect(concat!( "Expectected '", $prefix, "' prefix, while trying to fill '", stringify!($gate), "'" )) .trim(); $gate = Some((|$strp_line: &str| $callback)($strp_line)); $continue; } }; } macro_rules! parser { ( $iterator:ident, $debug:ident, $($prefix:literal @ $prefix_var:ident => {$($expr:tt)*};)* $(;)? .. $diff:ident ) => { $( let $prefix_var = { let next = $iterator.next().unwrap(); if $debug { eprintln!(" {}", next); } next }.strip_prefix($prefix).unwrap().to_owned(); $( $expr )*; )* let $diff = { let mut diff = vec![]; let mut iter = $iterator.clone(); while let Some(next) = iter.next() { if next.starts_with("diff --git") { break; } if (next == "-- " || next == "--") && iter.peek().map(|v| v.starts_with("2")) == Some(true) { break; } diff.push(next); } diff }; for _ in 0..$diff.len() { let next = $iterator.next().unwrap(); if $debug { eprintln!(" {}", next); } } let $diff = $diff.join("\n"); }; } pub(crate) fn parse_file(file: &Path, debug: bool) -> Result { if debug { eprintln!("Started parsing `{}`..", file.display()); } let patch_text = fs::read_to_string(file)?; let (commit_message, date, mut cut_idx) = { let mut commit = None; let mut date = None; let mut author = None; let mut final_message = None; let mut finished_headers = false; let mut cut_idx = None; for (idx, line) in patch_text.lines().enumerate() { expect!(if commit -> "From " from line; assign |strp_line| { let (id, fake_date) = strp_line.split_once(' ').unwrap(); assert_eq!(fake_date, "Mon Sep 17 00:00:00 2001"); id.to_owned() }); expect!(if author -> "From: " from line; assign |strp_line| { strp_line.to_owned() }); expect!(if date -> "Date: " from line; assign |strp_line| { strp_line.to_owned() }); expect!(if final_message -> "Subject: " from line; assign |strp_line| { if strp_line.starts_with('[') { let chars = strp_line.chars().take_while(|ch| *ch != ']').collect::(); assert_eq!(&chars[1..=5], "PATCH"); strp_line[chars.len()+2..].to_owned() } else { unreachable!("Email subjects always start with [PATCH x/y]"); } }); if line.starts_with("MIME-Version: ") || line.starts_with("Content-Type: ") || line.starts_with("Content-Transfer-Encoding: ") { continue; } if line.starts_with(" ") { // this line is a continuation of the previous header. We just assume // it's the subject header. final_message.as_mut().unwrap().push_str(line); continue; } if line.is_empty() { // add the newline to end the subject message final_message.as_mut().unwrap().push('\n'); // and one newline to mark the end of the header and the start for more text. final_message.as_mut().unwrap().push('\n'); finished_headers = true; continue; } if line == "---" { cut_idx = Some(idx); break; } if finished_headers { // If we finished all the headers, the rest must be commit message stuff. // One per line. final_message.as_mut().unwrap().push_str(line); final_message.as_mut().unwrap().push('\n'); } else { bail!("Failed to recognize commit message header: `{line}`, while parsing patch.") } } if debug { eprintln!("Patch data:"); eprintln!(" Author: {}", author.unwrap()); eprintln!(" Commit: {}", commit.unwrap()); eprintln!(" Date: {}", date.as_ref().unwrap()); for line in final_message.as_ref().unwrap().lines() { eprintln!(" {}", line); } eprintln!("****************************"); } ( final_message.unwrap().trim().to_owned(), date.unwrap().trim().to_owned(), cut_idx.unwrap(), ) }; let mut patch_files = vec![]; let mut final_whitespace = None; let mut generating_version = None; let mut skipping_frontmatter = true; let mut iterator = patch_text .lines() .skip(cut_idx) // skip one more to go over the `---` line .skip(1) .peekable(); while let Some(line) = iterator.peek() { if debug { eprintln!("{}", line); } if (line.starts_with(" ") || line.is_empty()) && skipping_frontmatter { iterator.next().unwrap(); cut_idx += 1; continue; } if line.starts_with("diff --git") { if skipping_frontmatter { skipping_frontmatter = false; } iterator.next().unwrap(); continue; } match line { line if line.starts_with("new file mode") => { parser! { iterator, debug, "new file mode " @ mode => {let new_file_mode = mode.to_owned()}; "index " @ index => {let index = index.to_owned()}; "--- " @ from => {assert_eq!(from, "/dev/null")}; "+++ b/" @ to => {let path = PathBuf::from(to)}; ..diff } patch_files.push(FileDiff::Add { path, diff, index, new_file_mode, }) } line if line.starts_with("index") => { parser! { iterator, debug, "index " @ index => {()}; "--- a/" @ from => {()}; "+++ b/" @ to => {assert_eq!(from, to); let path = PathBuf::from(to)}; ..diff } patch_files.push(FileDiff::Change { path, diff, index }) } line if line.starts_with("similarity index 100%") => { parser! { iterator, debug, "similarity index 100%" @ none => {assert!(none.is_empty())}; "rename from " @ from => {let from = PathBuf::from(from)}; "rename to " @ to => {let to = PathBuf::from(to)}; .. no_diff } assert!(no_diff.is_empty()); patch_files.push(FileDiff::RenameNoChange { from, to }) } line if line.starts_with("similarity index ") => { parser! { iterator, debug, "similarity index " @ similarity => {()}; "rename from " @ from_1 => {let from = PathBuf::from(&from_1)}; "rename to " @ to_1 => {let to = PathBuf::from(&to_1)}; "index " @ index => {()}; "--- a/" @ from_2 => {assert_eq!(from_1, from_2)}; "+++ b/" @ to_2 => {assert_eq!(to_1, to_2)}; .. diff } patch_files.push(FileDiff::RenameWithChange { from, to, similarity, diff, index, }) } line if line.starts_with("deleted") => { parser! { iterator, debug, "deleted file mode " @ deleted_mode => {()}; "index " @ index => {()}; "--- a/" @ from => {let path = PathBuf::from(from)}; "+++ /dev/null" @ rest => {assert!(rest.is_empty())}; ..diff } patch_files.push(FileDiff::Remove { path, diff, index, deleted_mode, }) } line if *line == "--" || *line == "-- " => { final_whitespace = Some( match *line { "-- " => " ", "--" => "", _ => unreachable!(), }); iterator.next().unwrap(); generating_version = Some(iterator.next().unwrap().to_owned()); assert_eq!(iterator.next(), Some("")); assert_eq!(iterator.next(), None); break; } _ => { unreachable!("All possible line starts should be covered, but: `{line}` is not.") } } } let patch = Patch { files: patch_files, commit_message, date, final_whitespace: final_whitespace.unwrap(), generating_version: generating_version.unwrap(), }; let patch_text = patch_text .lines() .skip(cut_idx) // skip one more to go over the `---` line .skip(1) .collect::>() .join("\n"); let check_content = patch.to_string(); assert_eq!( check_content, patch_text, "Expected parsed patchtext to be the same as our the original patch" ); Ok(patch) }