use std::{ io::Write, path::PathBuf, process::{Command, Stdio}, }; use anyhow::{Context, Result, bail}; use clap::Parser; use crate::parse::{FileDiff, Patch}; mod parse; #[derive(Parser)] struct Cli { /// The file to pull the patch to rollup from. patch_files: Vec, /// Which repository to use as a target for the rollup /// /// Needs to be clean. #[arg(short, long)] to: PathBuf, #[arg(short, long)] debug: bool, } macro_rules! git { (@base for $finalizer:ident in $to:expr; $($args:literal),*) => { Command::new("git") .args([ "-C", $to.display().to_string().as_str(), $($args),* ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .$finalizer() .context(concat!("Failed to run `git", $($args),*, "`"))? }; (@handle_status $status:expr; $($args:literal),*) => {{ if !$status.success() { bail!(concat!("`git", $($args),*, "` failed!")) } }}; (@get_output in $to:expr; $($args:literal),*) => {{ let output = git!(@base for output in $to; $($args),*); git!(@handle_status output.status; $($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),*); 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),*); 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) }}; } fn main() -> Result<()> { let args = Cli::parse(); let mut patches = vec![]; for file in &args.patch_files { patches.push(parse::parse_file(file, args.debug)?); } eprintln!("Successfully parsed all {} patch files.", patches.len()); { let (stdout, _) = git!(@get_output in args.to; "status", "--porcelain"); if !stdout.is_empty() { bail!( "`--to` (`{}`) workspace is dirty, refusing to continue.", args.to.display() ); } } for (idx, patch) in patches.iter().enumerate() { eprintln!( "Applying {}. patch: `{}`... ", idx + 1, args.patch_files[idx].display() ); let mut final_diffs = vec![]; for file_diff in &patch.files { match file_diff { FileDiff::Add { path, .. } => { if args.to.join(path).exists() { bail!("A file was added, that already existed. This should be impossible.") } final_diffs.push(file_diff.to_owned()); } FileDiff::Remove { path, .. } => { if args.to.join(path).exists() { final_diffs.push(file_diff.to_owned()); } else { eprintln!( " > Dropping remove of `{}`, as the file does not exist in the rollup target.", path.display() ); } } FileDiff::Rename { from, to, .. } => { if args.to.join(from).exists() { if args.to.join(to).exists() { bail!( "A rename (`{}` -> `{}`) tried to rename a path to an already existing path. This should be impossible.", from.display(), to.display() ) } final_diffs.push(file_diff.to_owned()); } else { eprintln!( " > Dropping rename from `{}` to `{}`, as the from file does not exist in the rollup target.", from.display(), to.display() ); } } FileDiff::Change { path, .. } => { if args.to.join(path).exists() { final_diffs.push(file_diff.to_owned()); } else { eprintln!( " > Dropping change to `{}`, as the file does not exist in the rollup target.", path.display() ); } } } } let filtered_patch = Patch { files: final_diffs, commit_message: patch.commit_message.clone(), }; git!(@set_stdin_to filtered_patch; in args.to; "apply", "-"); 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}"); } } Ok(()) }