summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSoispha Peetz <soispha@b-peetz.de>2026-11-11 19:29:59 +0100
committerSoispha Peetz <soispha@b-peetz.de>2026-11-14 18:35:52 +0100
commit9e22855a1a9561e99c86bd5b61fe2fa0a4f77b39 (patch)
treebbc32709f1dc8e6ac3a7f5c420ca608756d54da9
parentpkgs/rollup: Support a `--dry-run` flag (diff)
downloadnixos-config-9e22855a1a9561e99c86bd5b61fe2fa0a4f77b39.zip
pkgs/rollup: Add config, specifying clear ownership based on targets
Thus applies are possible to ignore certain directories for certain targets.
-rw-r--r--pkgs/by-name/ro/rollup/Cargo.lock112
-rw-r--r--pkgs/by-name/ro/rollup/Cargo.toml2
-rw-r--r--pkgs/by-name/ro/rollup/src/config.rs79
-rw-r--r--pkgs/by-name/ro/rollup/src/main.rs160
4 files changed, 319 insertions, 34 deletions
diff --git a/pkgs/by-name/ro/rollup/Cargo.lock b/pkgs/by-name/ro/rollup/Cargo.lock
index 4184cc11..e4994453 100644
--- a/pkgs/by-name/ro/rollup/Cargo.lock
+++ b/pkgs/by-name/ro/rollup/Cargo.lock
@@ -111,12 +111,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -163,6 +185,47 @@ dependencies = [
"anyhow",
"clap",
"pretty_assertions",
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c187e0a369471c550af028cba56d4f4fa2a489ad28151ae95a4d340a2a8b782"
+dependencies = [
+ "serde_core",
]
[[package]]
@@ -173,9 +236,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
-version = "3.0.9"
+version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79"
dependencies = [
"proc-macro2",
"quote",
@@ -183,6 +246,45 @@ dependencies = [
]
[[package]]
+name = "toml"
+version = "1.2.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5755c51863cdac711f1956baf8eb9588e060755f7d3c4c979e70ea1e1f32cd7"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.2.0+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a"
+
+[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -210,6 +312,12 @@ dependencies = [
]
[[package]]
+name = "winnow"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef"
+
+[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/pkgs/by-name/ro/rollup/Cargo.toml b/pkgs/by-name/ro/rollup/Cargo.toml
index 51fecd59..0ce87401 100644
--- a/pkgs/by-name/ro/rollup/Cargo.toml
+++ b/pkgs/by-name/ro/rollup/Cargo.toml
@@ -19,3 +19,5 @@ homepage = ""
clap = { version = "4.6", features = ["derive"] }
anyhow = "1.0.104"
pretty_assertions = "1.4.1"
+serde = { version = "1.0.229", features = ["derive"] }
+toml = "1.2.1"
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<Owner>,
+ pub(crate) drop_ins: HashMap<PathBuf, DropInConfig>,
+}
+
+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<MainConfig> {
+ 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<Vec<(PathBuf, DropInConfig)>> {
+ 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<PathBuf>,
/// 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<Option<PathBuf>> {
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<Option<PathBuf>> {
fn apply_patches(
patch_files: Vec<PathBuf>,
- 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<PathBuf, DropInConfig>,
+ owner: &Owner,
+ root: &Path,
+ path: &Path,
+) -> Result<bool> {
+ 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()
+ );
+}