From 9e22855a1a9561e99c86bd5b61fe2fa0a4f77b39 Mon Sep 17 00:00:00 2001 From: Soispha Peetz Date: Wed, 11 Nov 2026 19:29:59 +0100 Subject: pkgs/rollup: Add config, specifying clear ownership based on targets Thus applies are possible to ignore certain directories for certain targets. --- pkgs/by-name/ro/rollup/src/config.rs | 79 +++++++++++++++++ pkgs/by-name/ro/rollup/src/main.rs | 160 ++++++++++++++++++++++++++++------- 2 files changed, 207 insertions(+), 32 deletions(-) create mode 100644 pkgs/by-name/ro/rollup/src/config.rs (limited to 'pkgs/by-name/ro/rollup/src') diff --git a/pkgs/by-name/ro/rollup/src/config.rs b/pkgs/by-name/ro/rollup/src/config.rs new file mode 100644 index 00000000..64e0b792 --- /dev/null +++ b/pkgs/by-name/ro/rollup/src/config.rs @@ -0,0 +1,79 @@ +use std::{ + collections::HashMap, + ffi::OsStr, + fs::{self}, + path::PathBuf, +}; + +use anyhow::{Result, bail}; +use serde::{Deserialize, Serialize}; + +pub(crate) const MAIN_CONFIG_NAME: &str = "rollup.toml"; +pub(crate) const DROP_IN_NAME: &str = ".rollup.owner"; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct MainConfig { + pub(crate) owners: Vec, + pub(crate) drop_ins: HashMap, +} + +impl MainConfig { + pub(crate) fn get_owner(&self, owner: &str) -> Result<&Owner> { + if let Some(own) = self.owners.iter().find(|o| o.name == owner) { + Ok(own) + } else { + bail!("Failed to find owner with name: `{}`", owner) + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Owner { + pub(crate) name: String, + pub(crate) applies_to: PathBuf, + pub(crate) last_applied_tag: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct DropInConfig { + pub(crate) owner: String, +} + +pub(crate) fn load(root: &PathBuf) -> Result { + let mut main: MainConfig = { + let path = root.join(MAIN_CONFIG_NAME); + let content = fs::read_to_string(path)?; + toml::from_str(&content)? + }; + + let drop_ins = collect_drop_ins(root)?; + + for (path, config) in drop_ins { + assert!(main.drop_ins.insert(path, config).is_none()); + } + + Ok(main) +} + +fn collect_drop_ins(path: &PathBuf) -> Result> { + let mut output = vec![]; + for entry in fs::read_dir(path)? { + let entry = entry?; + let ft = entry.file_type()?; + + if ft.is_dir() { + output.extend(collect_drop_ins(&entry.path())?); + } else if ft.is_file() && entry.file_name() == OsStr::new(DROP_IN_NAME) { + let drop_in: DropInConfig = { + let content = fs::read_to_string(entry.path())?; + toml::from_str(&content)? + }; + output.push(( + entry.path().parent().expect("All drop-ins have a parent").to_owned(), + drop_in, + )); + } + } + + Ok(output) +} diff --git a/pkgs/by-name/ro/rollup/src/main.rs b/pkgs/by-name/ro/rollup/src/main.rs index 48bbc842..090ac12c 100644 --- a/pkgs/by-name/ro/rollup/src/main.rs +++ b/pkgs/by-name/ro/rollup/src/main.rs @@ -1,5 +1,7 @@ use std::{ + collections::HashMap, env::current_dir, + ffi::OsStr, fs, io::Write, path::{Path, PathBuf}, @@ -9,8 +11,12 @@ use std::{ use anyhow::{Context, Result, bail}; use clap::{Parser, Subcommand}; -use crate::parse::{FileDiff, Patch}; +use crate::{ + config::{DROP_IN_NAME, DropInConfig, MAIN_CONFIG_NAME, MainConfig, Owner}, + parse::{FileDiff, Patch}, +}; +mod config; mod parse; #[derive(Parser)] @@ -31,10 +37,11 @@ enum Cmd { patch_files: Vec, /// Which repository to use as a target for the rollup + /// It is specified via the owner, declared in the rollup.toml config. /// /// The repository needs to be clean. #[arg(short, long)] - to: PathBuf, + to: String, /// Show debug information related to the parsing of patch files. #[arg(short, long)] @@ -43,22 +50,23 @@ enum Cmd { /// 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, + /// Which repository to use for the tag to base created tags on. + /// It is specified via the owner, declared in the rollup.toml config. + /// + /// The repository needs to be clean. + #[arg(short, long)] + to: 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 + /// Which repository to use as a target for the rollup and the tag for new + /// patches. + /// It is specified via the owner, declared in the rollup.toml config. /// /// The repository needs to be clean. #[arg(short, long)] - to: PathBuf, + to: String, /// Show debug information related to the parsing of patch files. #[arg(short, long)] @@ -124,6 +132,16 @@ macro_rules! git { fn main() -> Result<()> { let args = Cli::parse(); + let root = { + let (stdout, _) = git!(in current_dir()?; "rev-parse", "--show-toplevel"); + PathBuf::from(stdout.trim()) + }; + let config = config::load(&root).with_context(|| { + format!( + "Failed to load the config (`{MAIN_CONFIG_NAME}`), in root: `{}`", + root.display() + ) + })?; match args.subcommand { Cmd::Apply { @@ -131,21 +149,23 @@ fn main() -> Result<()> { to, debug_parse, } => { - apply_patches(patch_files, owner, debug_parse, args.dry_run)? + let owner = config.get_owner(&to)?; + check_owner(owner)?; + apply_patches(patch_files, owner, debug_parse, &config, root, args.dry_run)? } - create_patches(tag, args.dry_run)?; + Cmd::Create { to } => { + let owner = config.get_owner(&to)?; + create_patches(&owner.last_applied_tag, args.dry_run)?; } - Cmd::CreateApply { - tag, - to, - debug_parse, - } => { - check_to(&to)?; + Cmd::CreateApply { to, debug_parse } => { + let owner = config.get_owner(&to)?; + check_owner(owner)?; - let Some(patches_directory) = create_patches(tag, args.dry_run)? + let Some(patches_directory) = create_patches(&owner.last_applied_tag, args.dry_run)? else { return Ok(()); }; + let patch_files = { let iter = fs::read_dir(&patches_directory)?; let mut output = vec![None; iter.count()]; @@ -176,8 +196,10 @@ fn main() -> Result<()> { }; apply_patches( patch_files.into_iter().map(|s| s.unwrap()).collect(), - to, + owner, debug_parse, + &config, + root, args.dry_run, )?; } @@ -189,7 +211,7 @@ fn main() -> Result<()> { fn create_patches(tag: &str, dry_run: bool) -> Result> { let current_dir = current_dir()?; - let (stdout, _) = git!(in current_dir; "tag", "--contains", "HEAD", tag.as_str()); + let (stdout, _) = git!(in current_dir; "tag", "--contains", "HEAD", tag); if !stdout.is_empty() { println!( "There are no tags to generate. Tag {} already points to HEAD.", @@ -224,10 +246,14 @@ fn create_patches(tag: &str, dry_run: bool) -> Result> { fn apply_patches( patch_files: Vec, - to: PathBuf, + owner: &Owner, debug_parse: bool, + config: &MainConfig, + root: PathBuf, dry_run: bool, ) -> Result<()> { + let to = &owner.applies_to; + let mut patches = vec![]; for file in &patch_files { patches.push(parse::parse_file(file, debug_parse)?); @@ -259,13 +285,15 @@ fn apply_patches( 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()); + if drop_in_applies(&config.drop_ins, owner, &root, path)? { + final_diffs.push(file_diff.to_owned()); + } } FileDiff::Remove { path, .. } => { if to.join(path).exists() { - final_diffs.push(file_diff.to_owned()); - // eprintln!(" > Keeping remove of `{}`", path.display()); + if drop_in_applies(&config.drop_ins, owner, &root, path)? { + final_diffs.push(file_diff.to_owned()); + } } else { eprintln!( " > Dropping remove of `{}`, as the file does not exist in the rollup target.", @@ -283,8 +311,9 @@ fn apply_patches( to.display() ) } - final_diffs.push(file_diff.to_owned()); - // eprintln!(" > Keeping rename of `{}` to `{}`", from.display(), to.display()); + if drop_in_applies(&config.drop_ins, owner, &root, from)? { + final_diffs.push(file_diff.to_owned()); + } } else { eprintln!( " > Dropping rename from `{}` to `{}`, as the from file does not exist in the rollup target.", @@ -295,7 +324,9 @@ fn apply_patches( } FileDiff::Change { path, .. } => { if to.join(path).exists() { - final_diffs.push(file_diff.to_owned()); + if drop_in_applies(&config.drop_ins, owner, &root, path)? { + final_diffs.push(file_diff.to_owned()); + } } else { eprintln!( " > Dropping change to `{}`, as the file does not exist in the rollup target.", @@ -317,7 +348,7 @@ fn apply_patches( }; if dry_run { - eprintln!("Dry-run is on; Not actually applying, adding and commiting"); + eprintln!("Dry-run is on; Not actually applying, adding or commiting"); } else { git!(@set_stdin_to filtered_patch; in to; "apply", "-", "--whitespace=nowarn"); git!(in to; "add", "."); @@ -328,7 +359,8 @@ fn apply_patches( Ok(()) } -fn check_to(to: &Path) -> Result<()> { +fn check_owner(owner: &Owner) -> Result<()> { + let to = &owner.applies_to; let to = to.join(".git"); // NOTE: We cannot just try to stat() the path, because landlock might be in use and does not @@ -350,3 +382,67 @@ fn check_to(to: &Path) -> Result<()> { Ok(()) } + +fn drop_in_applies( + drop_ins: &HashMap, + owner: &Owner, + root: &Path, + path: &Path, +) -> Result { + let path = &path.canonicalize()?; + + let file_name = path.file_name().expect("All paths are to files"); + if file_name == OsStr::new(DROP_IN_NAME) || file_name == OsStr::new(MAIN_CONFIG_NAME) { + eprintln!( + "Not applying change to `{}`, because it's a rollup config or drop in.", + path.display() + ); + + return Ok(false); + } + + if path.parent() == Some(root) { + return Ok(true); + } + + let path = PathBuf::from( + &path + .display() + .to_string() + .strip_prefix(format!("{}/", root.display()).as_str()) + .expect("All paths are in root"), + ); + + if let Some(conf) = drop_ins.get(&path) + && conf.owner == owner.name + { + return Ok(true); + } + + let mut parent: &Path = &path; + while let Some(next) = parent.parent() { + // eprintln!("Checking `{}` for drop-ins..", next.display()); + + if let Some(conf) = drop_ins.get(&root.join(next)) { + // eprint!("Found a drop-in for `{}`", next.display()); + if conf.owner != owner.name { + // eprintln!(", but it's for `{}`, so ignoring path.", conf.owner); + eprintln!( + "`{}` says this dir is for `{}`, so ignoring path.", + next.join(DROP_IN_NAME).display(), + conf.owner + ); + return Ok(false); + } else { + // eprintln!(", which matches."); + return Ok(true); + } + } + parent = next; + } + + bail!( + "There was no drop-in config defining path ownership for: `{}`", + path.display() + ); +} -- cgit v1.3.1