summaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ro/rollup/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ro/rollup/src/main.rs')
-rw-r--r--pkgs/by-name/ro/rollup/src/main.rs315
1 files changed, 315 insertions, 0 deletions
diff --git a/pkgs/by-name/ro/rollup/src/main.rs b/pkgs/by-name/ro/rollup/src/main.rs
new file mode 100644
index 00000000..ed4e29b4
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/src/main.rs
@@ -0,0 +1,315 @@
+use std::{
+ env::current_dir,
+ fs,
+ io::Write,
+ path::{Path, PathBuf},
+ process::{Command, Stdio},
+};
+
+use anyhow::{Context, Result, bail};
+use clap::{Parser, Subcommand};
+
+use crate::parse::{FileDiff, Patch};
+
+mod parse;
+
+#[derive(Parser)]
+struct Cli {
+ #[command(subcommand)]
+ subcommand: Cmd,
+}
+
+#[derive(Subcommand)]
+enum Cmd {
+ /// Apply patches to a rollup target.
+ Apply {
+ /// The files to pull the patches to rollup from.
+ patch_files: Vec<PathBuf>,
+
+ /// Which repository to use as a target for the rollup
+ ///
+ /// The repository needs to be clean.
+ #[arg(short, long)]
+ to: PathBuf,
+
+ /// Show debug information related to the parsing of patch files.
+ #[arg(short, long)]
+ debug_parse: bool,
+ },
+
+ /// Create the patch files (based on the given tag)
+ Create {
+ /// Which tag to use to mark the already applied patches.
+ #[arg(default_value = "CONFIG_APPLIED")]
+ tag: String,
+ },
+
+ /// First create the patches, and then automatically apply them.
+ CreateApply {
+ /// Which tag to use to mark the already applied patches.
+ #[arg(default_value = "CONFIG_APPLIED")]
+ tag: String,
+
+ /// Which repository to use as a target for the rollup
+ ///
+ /// The repository needs to be clean.
+ #[arg(short, long)]
+ to: PathBuf,
+
+ /// Show debug information related to the parsing of patch files.
+ #[arg(short, long)]
+ debug_parse: bool,
+ },
+}
+
+macro_rules! git {
+ (@_base for $finalizer:ident in $to:expr; $($args:expr),*) => {
+ Command::new("git")
+ .args([
+ "-C",
+ $to.display().to_string().as_str(),
+ $($args),*
+ ])
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .$finalizer()
+ .with_context(|| format!("Failed to run {}", git!(@_fmt_cmd $($args),*)))?
+ };
+ (@_handle_status $output:expr; $($args:expr),*) => {{
+ if !$output.status.success() {
+ bail!(
+ "{} failed!\n\nStderr was:\n {}",
+ git!(@_fmt_cmd $($args),*),
+ String::from_utf8($output.stderr).unwrap()
+ )
+ }
+ 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)
+ }};
+ (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)
+ }};
+ (@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()
+ .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");
+
+ (stdout, stderr)
+ }};
+}
+
+fn main() -> Result<()> {
+ let args = Cli::parse();
+
+ match args.subcommand {
+ Cmd::Apply {
+ patch_files,
+ to,
+ debug_parse,
+ } => {
+ check_to(&to)?;
+ apply_patches(patch_files, to, debug_parse)?
+ }
+ Cmd::Create { tag } => {
+ create_patches(tag)?;
+ }
+ Cmd::CreateApply {
+ tag,
+ to,
+ debug_parse,
+ } => {
+ check_to(&to)?;
+
+ let Some(patches_directory) = create_patches(tag)? else {
+ return Ok(());
+ };
+ let patch_files = {
+ let iter = fs::read_dir(patches_directory)?;
+ let mut output = vec![];
+ for entry in iter {
+ let entry = entry?;
+ if !entry.file_type()?.is_file() {
+ unreachable!("Should not be able to get non-files in patches directory");
+ }
+ output.push(entry.path());
+ }
+ output.sort();
+ output
+ };
+ apply_patches(patch_files, to, debug_parse)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn create_patches(tag: String) -> Result<Option<PathBuf>> {
+ let current_dir = current_dir()?;
+
+ let (stdout, _) = git!(in current_dir; "tag", "--contains", "HEAD", tag.as_str());
+ if !stdout.is_empty() {
+ println!(
+ "There are no tags to generate. Tag {} already points to HEAD.",
+ tag
+ );
+ return Ok(None);
+ }
+
+ let mktemp = Command::new("mktemp")
+ .args(["-t", "rollup_patches_XXXXXX", "-d"])
+ .output()
+ .context("Failed to run `mktemp -t rollup_patches_XXXXXX -d`")?;
+ if !mktemp.status.success() {
+ bail!("Failed to generate temp-directory for generated patches.");
+ }
+
+ let tempdir = PathBuf::from(String::from_utf8(mktemp.stdout).unwrap().trim());
+
+ git!(in current_dir; "format-patch", format!("{}..", tag).as_str(), "-o", tempdir.display().to_string().as_str());
+ git!(in current_dir; "tag", "--delete", tag.as_str());
+ git!(in current_dir; "tag", "--annotate", tag.as_str(), "-m", "");
+
+ println!("Patches have been generated here: `{}`", tempdir.display());
+
+ Ok(Some(tempdir))
+}
+
+fn apply_patches(patch_files: Vec<PathBuf>, to: PathBuf, debug_parse: bool) -> Result<()> {
+ let mut patches = vec![];
+ for file in &patch_files {
+ patches.push(parse::parse_file(file, debug_parse)?);
+ }
+
+ eprintln!("Successfully parsed all {} patch files.", patches.len());
+
+ {
+ let (stdout, _) = git!(in to; "status", "--porcelain");
+ if !stdout.is_empty() {
+ bail!(
+ "`--to` (`{}`) workspace is dirty, refusing to continue.",
+ to.display()
+ );
+ }
+ }
+
+ for (idx, patch) in patches.iter().enumerate() {
+ eprintln!(
+ "Applying {}. patch: `{}`... ",
+ idx + 1,
+ patch_files[idx].display()
+ );
+
+ let mut final_diffs = vec![];
+ for file_diff in &patch.files {
+ match file_diff {
+ FileDiff::Add { path, .. } => {
+ if to.join(path).exists() {
+ 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 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.",
+ path.display()
+ );
+ }
+ }
+ FileDiff::RenameNoChange { from, to, .. }
+ | FileDiff::RenameWithChange { from, to, .. } => {
+ if to.join(from).exists() {
+ if 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());
+ // 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.",
+ from.display(),
+ to.display()
+ );
+ }
+ }
+ FileDiff::Change { path, .. } => {
+ if 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()
+ );
+ }
+ }
+ }
+ }
+
+ if final_diffs.is_empty() {
+ eprintln!(" -> Patch is empty after dropping, skipping it.",);
+ continue;
+ }
+
+ let filtered_patch = Patch {
+ files: final_diffs,
+ ..patch.to_owned()
+ };
+
+ git!(@set_stdin_to filtered_patch; in to; "apply", "-", "--whitespace=nowarn");
+ git!(in to; "add", ".");
+ git!(@set_stdin_to filtered_patch.commit_message; in to; "commit", "--file=-", format!("--date={}", filtered_patch.date).as_str(), "--quiet");
+ }
+
+ Ok(())
+}
+
+fn check_to(to: &Path) -> Result<()> {
+ let to = to.join(".git");
+
+ // NOTE: We cannot just try to stat() the path, because landlock might be in use and does not
+ // currently deny that (tracked in: https://github.com/landlock-lsm/linux/issues/52) <2026-11-09>
+ // So we instead try to list it.
+ let is_listable = {
+ let dir_entries = fs::read_dir(&to).with_context(|| {
+ format!(
+ "Failed check regarding `--to` directory (`{}`) readability. Are you in a sandbox?",
+ to.display()
+ )
+ })?;
+ dir_entries.count() > 0
+ };
+
+ if !is_listable {
+ bail!("To directory (`{}`) is not readable", to.display());
+ }
+
+ Ok(())
+}