diff options
Diffstat (limited to 'pkgs/by-name/ro/rollup/src/parse.rs')
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/parse.rs | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/pkgs/by-name/ro/rollup/src/parse.rs b/pkgs/by-name/ro/rollup/src/parse.rs new file mode 100644 index 00000000..ac30eaa4 --- /dev/null +++ b/pkgs/by-name/ro/rollup/src/parse.rs @@ -0,0 +1,437 @@ +#![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<FileDiff>, + 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<Patch> { + 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::<String>(); + 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::<Vec<_>>() + .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) +} |
