use core::fmt; use std::{ fs, path::{Path, PathBuf}, process::Command, }; use anyhow::{Context, Result}; use pretty_assertions::assert_eq; pub(crate) struct Patch { pub(crate) files: Vec, pub(crate) commit_message: 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)?; } Ok(()) } } #[derive(Clone)] pub(crate) enum FileDiff { Add { path: PathBuf, diff: String, }, Remove { path: PathBuf, diff: String, index: String, deleted: String, }, Change { from: PathBuf, to: PathBuf, diff: String, index: String, }, } impl FileDiff { fn from_parts( from: PathBuf, to: PathBuf, diff: String, index: Option, deleted: Option, ) -> Self { let diff = diff.trim().to_owned(); if from.display().to_string() == "/dev/null" { Self::Add { path: to, diff } } else if to.display().to_string() == "/dev/null" { Self::Remove { path: from, diff, index: index.unwrap(), deleted: deleted.unwrap(), } } else { Self::Change { from, to, diff, index: index.unwrap(), } } } } impl fmt::Display for FileDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FileDiff::Add { path, diff } => { // writeln!(f, "diff --git /dev/null b/{}", path.display())?; writeln!(f, "--- /dev/null")?; writeln!(f, "+++ b/{}", path.display())?; writeln!(f, "{}", diff)?; } FileDiff::Remove { path, diff, index, deleted, } => { writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?; writeln!(f, "deleted {}", deleted)?; writeln!(f, "index {}", index)?; writeln!(f, "+++ /dev/null")?; writeln!(f, "{}", diff)?; } FileDiff::Change { from, to, diff, index, } => { writeln!(f, "diff --git a/{} b/{}", from.display(), 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; } }; } pub(crate) fn parse_file(file: &Path, debug: bool) -> Result { let data = Command::new("filterdiff") .arg(&file) .output() .context("Failed to run `filterdiff` for patch analysis")?; let filterdiff_output = String::from_utf8(data.stdout).unwrap(); if debug { eprintln!("{}", filterdiff_output); eprintln!("****************************"); } let commit_message = { let raw_message = fs::read_to_string(file)?; let mut commit = None; let mut date = None; let mut author = None; let mut final_message = None; for line in raw_message.lines() { 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()+1..].to_owned() } else { unreachable!("Email subjects always start with [PATCH x/y]"); } }); if line == "---" { break; } final_message.as_mut().unwrap().push_str(line); final_message.as_mut().unwrap().push('\n'); } if debug { eprintln!("Patch data:"); eprintln!(" Author: {}", author.unwrap()); eprintln!(" Commit: {}", commit.unwrap()); eprintln!(" Date: {}", date.unwrap()); for line in final_message.as_ref().unwrap().lines() { eprintln!(" {}", line); } eprintln!("****************************"); } final_message.unwrap().trim().to_owned() }; let mut diff = Patch { files: vec![], commit_message, }; let mut fd_from = None; let mut fd_to = None; let mut fd_diff: Option = None; let mut fd_index: Option = None; let mut fd_deleted: Option = None; let mut git_diff = None; for (idx, line) in filterdiff_output.lines().enumerate() { if debug { eprintln!("{}", line); } if line.starts_with("diff --git") { if idx != 0 { diff.files.push(FileDiff::from_parts( fd_from.take().unwrap(), fd_to.take().unwrap(), fd_diff.take().unwrap(), fd_index.take(), fd_deleted.take(), )); } git_diff = Some(line.to_owned()); continue; } if line.starts_with("deleted") { expect!(if fd_deleted -> "deleted" from line; assign |strp_line| { strp_line.to_owned() }); } if line.starts_with("index") { expect!(if fd_index -> "index" from line; assign |strp_line| { strp_line.to_owned() }); } if fd_deleted.is_some() { let git_diff = git_diff.clone().unwrap(); expect!(@no_wrap if fd_from -> "diff --git a/" from git_diff; assign |strp_line| { let (from, _to) = strp_line.split_once(' ').unwrap(); PathBuf::from(from) }); } else { expect!(if fd_from -> "---" from line; assign |strp_line| { if strp_line != "/dev/null" { PathBuf::from(strp_line.strip_prefix("a/").unwrap()) } else { PathBuf::from(strp_line) } }); } expect!(if fd_to -> "+++" from line; assign |strp_line| { if strp_line != "/dev/null" { PathBuf::from(strp_line.strip_prefix("b/").unwrap()) } else { PathBuf::from(strp_line) } }); match fd_diff.as_mut() { Some(str) => { str.push_str(line); str.push('\n'); } None => { let mut base = line.to_owned(); base.push('\n'); fd_diff = Some(base); } } } diff.files.push(FileDiff::from_parts( fd_from.take().unwrap(), fd_to.take().unwrap(), fd_diff.take().unwrap(), fd_index.take(), fd_deleted.take(), )); let check_content = diff.to_string(); assert_eq!(filterdiff_output, check_content); Ok(diff) }