diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-11-04 20:09:47 +0100 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-11-05 22:47:43 +0100 |
| commit | a5c07e033b4660e1aabf45ec42eb1e4b4c07db8f (patch) | |
| tree | ce362e2f9d35c40792c2d1bad17a4f50d8b3605d /pkgs/by-name/ro/rollup/src | |
| parent | pkgs/rollup: Init (diff) | |
| download | nixos-config-a5c07e033b4660e1aabf45ec42eb1e4b4c07db8f.zip | |
pkgs/rollup: Finish first mvp
Diffstat (limited to 'pkgs/by-name/ro/rollup/src')
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/main.rs | 200 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/parse.rs | 289 |
2 files changed, 409 insertions, 80 deletions
diff --git a/pkgs/by-name/ro/rollup/src/main.rs b/pkgs/by-name/ro/rollup/src/main.rs index 8285e702..48b395a5 100644 --- a/pkgs/by-name/ro/rollup/src/main.rs +++ b/pkgs/by-name/ro/rollup/src/main.rs @@ -1,105 +1,145 @@ -use core::fmt; -use std::{path::PathBuf, process::Command}; +use std::{ + io::Write, + path::PathBuf, + process::{Command, Stdio}, +}; -use anyhow::{Context, Result}; +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_file: PathBuf, -} + patch_files: Vec<PathBuf>, -struct Diff { - files: Vec<FileDiff>, -} - -struct FileDiff { - from: PathBuf, + /// Which repository to use as a target for the rollup + /// + /// Needs to be clean. + #[arg(short, long)] to: PathBuf, - diff: String, - index: String, -} -impl fmt::Display for FileDiff { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - "diff --git a/{} b/{}", - self.from.display(), - self.to.display() - )?; - writeln!(f, "--- a/{}", self.from.display())?; - writeln!(f, "+++ b/{}", self.to.display())?; - writeln!(f, "index {}", self.index)?; - writeln!(f, "{}", self.diff)?; - - Ok(()) - } -} - -macro_rules! take { - ($line:expr, $prefix:literal) => { - $line.strip_prefix($prefix).unwrap().trim() - }; + #[arg(short, long)] + debug: bool, } fn main() -> Result<()> { let args = Cli::parse(); - let data = Command::new("filterdiff") - .arg(&args.patch_file) - .output() - .context("Failed to run `filterdiff` for patch analysis")?; + let mut patches = vec![]; + for file in &args.patch_files { + patches.push(parse::parse_file(file, args.debug)?); + } - let stdout = String::from_utf8(data.stdout).unwrap(); + eprintln!("Successfully parsed all {} patch files.", patches.len()); - let mut diff = Diff { files: vec![] }; - let mut file_diff_from = None; - let mut file_diff_to = None; - let mut file_diff_diff: Option<String> = None; - let mut file_diff_index: Option<String> = None; - for (idx, line) in stdout.lines().enumerate() { - eprintln!("{}", line); + { + 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() + ); + } + } - if line.starts_with("---") { - file_diff_from = Some(PathBuf::from(take!(line, "--- a/"))); - } else if line.starts_with("+++") { - file_diff_to = Some(PathBuf::from(take!(line, "+++ b/"))); - } else if line.starts_with("index") { - file_diff_index = Some(take!(line, "index").to_owned()); - } else if line.starts_with("diff --git") { - if idx != 0 { - diff.files.push(FileDiff { - from: file_diff_from.take().unwrap(), - to: file_diff_to.take().unwrap(), - diff: file_diff_diff.take().unwrap(), - index: file_diff_index.take().unwrap(), - }); - } - } else { - match file_diff_diff.as_mut() { - Some(str) => { - str.push_str(line); - str.push('\n'); + 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()); } - None => { - let mut base = line.to_owned(); - base.push('\n'); - file_diff_diff = Some(base); + 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() + ); + } } } } - } - diff.files.push(FileDiff { - from: file_diff_from.unwrap(), - to: file_diff_to.unwrap(), - diff: file_diff_diff.unwrap(), - index: file_diff_index.unwrap(), - }); - for file in diff.files { - eprintln!("{}", file) + 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(()) 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..ceb9d44f --- /dev/null +++ b/pkgs/by-name/ro/rollup/src/parse.rs @@ -0,0 +1,289 @@ +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<FileDiff>, + 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<String>, + deleted: Option<String>, + ) -> 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<Patch> { + 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::<String>(); + 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<String> = None; + let mut fd_index: Option<String> = None; + let mut fd_deleted: Option<String> = 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) +} |
