diff options
Diffstat (limited to 'pkgs')
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/main.rs | 58 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/parse.rs | 405 |
2 files changed, 293 insertions, 170 deletions
diff --git a/pkgs/by-name/ro/rollup/src/main.rs b/pkgs/by-name/ro/rollup/src/main.rs index 4083259f..71a54049 100644 --- a/pkgs/by-name/ro/rollup/src/main.rs +++ b/pkgs/by-name/ro/rollup/src/main.rs @@ -27,7 +27,7 @@ struct Cli { } macro_rules! git { - (@base for $finalizer:ident in $to:expr; $($args:literal),*) => { + (@_base for $finalizer:ident in $to:expr; $($args:expr),*) => { Command::new("git") .args([ "-C", @@ -38,34 +38,38 @@ macro_rules! git { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .$finalizer() - .context(concat!("Failed to run `git", $($args),*, "`"))? + .with_context(|| format!("Failed to run {}", git!(@_fmt_cmd $($args),*)))? }; - (@handle_status $status:expr; $($args:literal),*) => {{ - if !$status.success() { - bail!(concat!("`git", $($args),*, "` failed!")) + (@_handle_status $output:expr; $($args:expr),*) => {{ + if !$output.status.success() { + bail!("{} failed!", git!(@_fmt_cmd $($args),*)) } + if !$output.stderr.is_empty() { + let stderr = String::from_utf8($output.stderr).expect("git should always produce utf8"); + bail!("{} returned some errors:\n{stderr}", git!(@_fmt_cmd $($args),*)); + } + }}; + (@_fmt_cmd $($args:expr),*) => {{ + let all = ["git", $($args),*].join(" "); + format!("`{}`", all) }}; - (@get_output in $to:expr; $($args:literal),*) => {{ - let output = git!(@base for output in $to; $($args),*); - git!(@handle_status output.status; $($args),*); + (in $to:expr; $($args:expr),*) => {{ + let output = git!(@_base for output in $to; $($args),*); + git!(@_handle_status output; $($args),*); let stdout = String::from_utf8(output.stdout).expect("git should always produce utf8"); let stderr = String::from_utf8(output.stderr).expect("git should always produce utf8"); (stdout, stderr) }}; - (in $to:expr; $($args:literal),*) => {{ - let status = git!(@base for status in $to; $($args),*); - git!(@handle_status status; $($args),*); - }}; - (@set_stdin_to $stdin:expr; in $to:expr; $($args:literal),*) => {{ - let mut child = git!(@base for spawn in $to; $($args),*); + (@set_stdin_to $stdin:expr; in $to:expr; $($args:expr),*) => {{ + let mut child = git!(@_base for spawn in $to; $($args),*); let mut stdin = child.stdin.take().expect("is piped"); write!(stdin, "{}", $stdin)?; drop(stdin); let output = child.wait_with_output() - .context(concat!("Failed to run `git", $($args),*, "`"))?; - git!(@handle_status output.status; $($args),*); + .with_context(|| format!("Failed to run `{}`", git!(@_fmt_cmd $($args),*)))?; + git!(@_handle_status output; $($args),*); let stdout = String::from_utf8(output.stdout).expect("git should always produce utf8"); let stderr = String::from_utf8(output.stderr).expect("git should always produce utf8"); @@ -85,7 +89,7 @@ fn main() -> Result<()> { eprintln!("Successfully parsed all {} patch files.", patches.len()); { - let (stdout, _) = git!(@get_output in args.to; "status", "--porcelain"); + let (stdout, _) = git!(in args.to; "status", "--porcelain"); if !stdout.is_empty() { bail!( "`--to` (`{}`) workspace is dirty, refusing to continue.", @@ -109,10 +113,12 @@ fn main() -> Result<()> { bail!("A file was added, that already existed. This should be impossible.") } final_diffs.push(file_diff.to_owned()); + // eprintln!(" > Keeping add of `{}`", path.display()); } FileDiff::Remove { path, .. } => { if args.to.join(path).exists() { final_diffs.push(file_diff.to_owned()); + // eprintln!(" > Keeping remove of `{}`", path.display()); } else { eprintln!( " > Dropping remove of `{}`, as the file does not exist in the rollup target.", @@ -120,7 +126,8 @@ fn main() -> Result<()> { ); } } - FileDiff::Rename { from, to, .. } => { + FileDiff::RenameNoChange { from, to, .. } + | FileDiff::RenameWithChange { from, to, .. } => { if args.to.join(from).exists() { if args.to.join(to).exists() { bail!( @@ -130,6 +137,7 @@ fn main() -> Result<()> { ) } final_diffs.push(file_diff.to_owned()); + // eprintln!(" > Keeping rename of `{}` to `{}`", from.display(), to.display()); } else { eprintln!( " > Dropping rename from `{}` to `{}`, as the from file does not exist in the rollup target.", @@ -151,17 +159,19 @@ fn main() -> Result<()> { } } + if final_diffs.is_empty() { + eprintln!(" -> Patch is empty after dropping, skipping it.",); + continue; + } + let filtered_patch = Patch { files: final_diffs, - commit_message: patch.commit_message.clone(), + ..patch.to_owned() }; - git!(@set_stdin_to filtered_patch; in args.to; "apply", "-"); + git!(@set_stdin_to filtered_patch; in args.to; "apply", "-", "--whitespace=nowarn"); git!(in args.to; "add", "."); - let (_, stderr) = git!(@set_stdin_to filtered_patch.commit_message; in args.to; "commit", "--file=-", "--quiet"); - if !stderr.is_empty() { - bail!("git commit returned some errors: \n{stderr}"); - } + git!(@set_stdin_to filtered_patch.commit_message; in args.to; "commit", "--file=-", format!("--date={}", filtered_patch.date).as_str(), "--quiet"); } Ok(()) diff --git a/pkgs/by-name/ro/rollup/src/parse.rs b/pkgs/by-name/ro/rollup/src/parse.rs index fb5cd05e..ac30eaa4 100644 --- a/pkgs/by-name/ro/rollup/src/parse.rs +++ b/pkgs/by-name/ro/rollup/src/parse.rs @@ -1,16 +1,21 @@ +#![allow(clippy::no_effect)] + use core::fmt; use std::{ fs, path::{Path, PathBuf}, - process::Command, }; -use anyhow::{Context, Result}; +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 { @@ -18,6 +23,8 @@ impl fmt::Display for Patch { for file in &self.files { write!(f, "{}", file)?; } + writeln!(f, "--{}", self.final_whitespace)?; + writeln!(f, "{}", self.generating_version)?; Ok(()) } @@ -28,67 +35,45 @@ pub(crate) enum FileDiff { Add { path: PathBuf, diff: String, + index: String, + new_file_mode: String, }, Remove { path: PathBuf, diff: String, index: String, - deleted: String, + deleted_mode: String, }, Change { path: PathBuf, diff: String, index: String, }, - Rename { + RenameNoChange { from: PathBuf, to: PathBuf, - diff: String, - similarity: String, }, -} -impl FileDiff { - fn from_parts( + RenameWithChange { from: PathBuf, to: PathBuf, + similarity: String, diff: String, - index: Option<String>, - deleted: Option<String>, - similarity: Option<String>, - ) -> Self { - let diff = diff[..diff.len() - 1].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 if from == to { - Self::Change { - path: from, - diff, - index: index.unwrap(), - } - } else { - Self::Rename { - from, - to, - diff, - similarity: similarity.unwrap(), - } - } - } + index: String, + }, } 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())?; + 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)?; @@ -97,11 +82,12 @@ impl fmt::Display for FileDiff { path, diff, index, - deleted, + deleted_mode, } => { writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?; - writeln!(f, "deleted {}", deleted)?; + writeln!(f, "deleted file mode {}", deleted_mode)?; writeln!(f, "index {}", index)?; + writeln!(f, "--- a/{}", path.display())?; writeln!(f, "+++ /dev/null")?; writeln!(f, "{}", diff)?; } @@ -112,15 +98,25 @@ impl fmt::Display for FileDiff { writeln!(f, "+++ b/{}", path.display())?; writeln!(f, "{}", diff)?; } - FileDiff::Rename { + 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, - diff, 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)?; } @@ -155,28 +151,67 @@ macro_rules! expect { }; } -pub(crate) fn parse_file(file: &Path, debug: bool) -> Result<Patch> { - let data = Command::new("filterdiff") - .arg(file) - .output() - .context("Failed to run `filterdiff` for patch analysis")?; +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 + }; - eprintln!("Started parsing `{}`..", file.display()); + for _ in 0..$diff.len() { + let next = $iterator.next().unwrap(); + if $debug { + eprintln!(" {}", next); + } + } + let $diff = $diff.join("\n"); - let filterdiff_output = String::from_utf8(data.stdout).unwrap(); + }; +} + +pub(crate) fn parse_file(file: &Path, debug: bool) -> Result<Patch> { if debug { - eprintln!("{}", filterdiff_output); - eprintln!("****************************"); + eprintln!("Started parsing `{}`..", file.display()); } + let patch_text = fs::read_to_string(file)?; - let commit_message = { - let raw_message = 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; - for line in raw_message.lines() { + 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"); @@ -192,133 +227,211 @@ pub(crate) fn parse_file(file: &Path, debug: bool) -> Result<Patch> { 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()+1..].to_owned() + 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; } - final_message.as_mut().unwrap().push_str(line); - final_message.as_mut().unwrap().push('\n'); + + 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.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() + ( + final_message.unwrap().trim().to_owned(), + date.unwrap().trim().to_owned(), + cut_idx.unwrap(), + ) }; - let mut diff = Patch { - files: vec![], - commit_message, - }; - let mut fd_from = None; - let mut fd_to = None; - let mut fd_diff: Option<String> = None; - let mut fd_index: Option<String> = None; - let mut fd_deleted: Option<String> = None; - let mut fd_similarity: Option<String> = None; - let mut git_diff = None; - for (idx, line) in filterdiff_output.lines().enumerate() { + 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 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(), - fd_similarity.take(), - )); + if skipping_frontmatter { + skipping_frontmatter = false; } - git_diff = Some(line.to_owned()); + iterator.next().unwrap(); continue; } - if line.starts_with("similarity index") { - expect!(if fd_similarity -> "similarity index" from line; assign |strp_line| { - strp_line.to_owned() - }); - } - if line.starts_with("rename from") { - expect!(if fd_from -> "rename from" from line; assign |strp_line| { - assert_ne!(strp_line, "/dev/null"); - PathBuf::from(strp_line) - }); - } - if line.starts_with("deleted") { - expect!(if fd_deleted -> "deleted" from line; assign |strp_line| { - strp_line.to_owned() - }); - } + 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()); - if line.starts_with("index") { - expect!(if fd_index -> "index" from line; assign |strp_line| { - strp_line.to_owned() - }); - } + 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 + } - 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) + 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 } - }); - } - 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) + patch_files.push(FileDiff::Remove { + path, + diff, + index, + deleted_mode, + }) } - }); - - match fd_diff.as_mut() { - Some(str) => { - str.push_str(line); - str.push('\n'); + 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; } - None => { - let mut base = line.to_owned(); - base.push('\n'); - fd_diff = Some(base); + _ => { + unreachable!("All possible line starts should be covered, but: `{line}` is not.") } } } - diff.files.push(FileDiff::from_parts( - fd_from.take().unwrap(), - fd_to.take().unwrap(), - fd_diff.take().unwrap(), - fd_index.take(), - fd_deleted.take(), - fd_similarity.take(), - )); - let check_content = diff.to_string(); + 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!(filterdiff_output, check_content); + assert_eq!( + check_content, patch_text, + "Expected parsed patchtext to be the same as our the original patch" + ); - Ok(diff) + Ok(patch) } |
