summaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ro/rollup/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-11-04 20:09:47 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-11-05 22:47:43 +0100
commita5c07e033b4660e1aabf45ec42eb1e4b4c07db8f (patch)
treece362e2f9d35c40792c2d1bad17a4f50d8b3605d /pkgs/by-name/ro/rollup/src
parentpkgs/rollup: Init (diff)
downloadnixos-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.rs200
-rw-r--r--pkgs/by-name/ro/rollup/src/parse.rs289
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)
+}