use std::{ io::Write, path::PathBuf, process::{Command, Stdio}, }; use anyhow::{Context, Result, bail}; use clap::Parser; use crate::parse::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, } 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 git_check = Command::new("git") .args([ "-C", args.to.display().to_string().as_str(), "status", "--porcelain", ]) .output() .context("Failed to run `git status --procelain`")?; if !git_check.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 { parse::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()); } parse::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() ); } } parse::FileDiff::Change { from, to, .. } => { if args.to.join(from).exists() { if from != to && 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 change to `{}`, as the file does not exist in the rollup target.", from.display() ); } } } } let filtered_patch = Patch { files: final_diffs, commit_message: patch.commit_message.clone(), }; let mut apply_child = Command::new("git") .args(["-C", args.to.display().to_string().as_str(), "apply", "-"]) .stdin(Stdio::piped()) .spawn()?; let mut stdin = apply_child.stdin.take().expect("is piped"); write!(stdin, "{}", filtered_patch)?; drop(stdin); let status = apply_child.wait()?; if !status.success() { bail!("git apply failed.") } let add = Command::new("git") .args(["-C", args.to.display().to_string().as_str(), "add", "."]) .status()?; if !add.success() { bail!("git add ., failed.") } let mut commit_child = Command::new("git") .args([ "-C", args.to.display().to_string().as_str(), "commit", "--file=-", ]) .stdin(Stdio::piped()) .spawn()?; let mut stdin = commit_child.stdin.take().expect("is piped"); write!(stdin, "{}", filtered_patch.commit_message)?; drop(stdin); let status = apply_child.wait()?; if !status.success() { bail!("git commit failed.") } } Ok(()) }