summaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ro
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ro')
-rw-r--r--pkgs/by-name/ro/rollup/.envrc21
-rw-r--r--pkgs/by-name/ro/rollup/.gitignore16
-rw-r--r--pkgs/by-name/ro/rollup/Cargo.lock216
-rw-r--r--pkgs/by-name/ro/rollup/Cargo.toml21
-rw-r--r--pkgs/by-name/ro/rollup/package.unstable.nix40
-rw-r--r--pkgs/by-name/ro/rollup/src/main.rs334
-rw-r--r--pkgs/by-name/ro/rollup/src/parse.rs437
-rwxr-xr-xpkgs/by-name/ro/rollup/update.sh14
8 files changed, 1099 insertions, 0 deletions
diff --git a/pkgs/by-name/ro/rollup/.envrc b/pkgs/by-name/ro/rollup/.envrc
new file mode 100644
index 00000000..0001f149
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/.envrc
@@ -0,0 +1,21 @@
+#!/usr/bin/env sh
+
+# Mpdpopm - A mpd rating tracker
+#
+# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
+# This file is part of Mpdpopm.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+source_env_if_exists ./.nenvrc
+
+PATH_add ./scripts
+PATH_add ./target/debug/
+PATH_add ./target/release/
+
+if on_git_branch; then
+ echo && git status --short --branch
+fi
diff --git a/pkgs/by-name/ro/rollup/.gitignore b/pkgs/by-name/ro/rollup/.gitignore
new file mode 100644
index 00000000..c80d7eef
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/.gitignore
@@ -0,0 +1,16 @@
+# Mpdpopm - A mpd rating tracker
+#
+# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
+# This file is part of Mpdpopm.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+# build
+/target
+/result
+
+# dev env
+.direnv
diff --git a/pkgs/by-name/ro/rollup/Cargo.lock b/pkgs/by-name/ro/rollup/Cargo.lock
new file mode 100644
index 00000000..4184cc11
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/Cargo.lock
@@ -0,0 +1,216 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rollup"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "pretty_assertions",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
diff --git a/pkgs/by-name/ro/rollup/Cargo.toml b/pkgs/by-name/ro/rollup/Cargo.toml
new file mode 100644
index 00000000..51fecd59
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/Cargo.toml
@@ -0,0 +1,21 @@
+# Mpdpopm - A mpd rating tracker
+#
+# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
+# This file is part of Mpdpopm.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+[package]
+name = "rollup"
+version = "0.1.0"
+edition = "2024"
+license = "AGPL-3.0-or-later"
+homepage = ""
+
+[dependencies]
+clap = { version = "4.6", features = ["derive"] }
+anyhow = "1.0.104"
+pretty_assertions = "1.4.1"
diff --git a/pkgs/by-name/ro/rollup/package.unstable.nix b/pkgs/by-name/ro/rollup/package.unstable.nix
new file mode 100644
index 00000000..2fdc7f52
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/package.unstable.nix
@@ -0,0 +1,40 @@
+# nixos-config - My current NixOS configuration
+#
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of my nixos-config.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+{
+ rustPlatform,
+ lib,
+ git,
+ patchutils,
+ makeWrapper,
+ mktemp,
+}:
+rustPlatform.buildRustPackage (finalAttrs: {
+ pname = "rollup";
+ version = "0.1.0";
+
+ buildInputs = [];
+ nativeBuildInputs = [
+ makeWrapper
+ ];
+
+ src = ./.;
+ cargoLock = {
+ lockFile = ./Cargo.lock;
+ };
+
+ postInstall = ''
+ wrapProgram $out/bin/rollup \
+ --set PATH ${lib.makeBinPath [git patchutils mktemp]}
+ '';
+
+ meta = {
+ mainProgram = "rollup";
+ };
+})
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..29746e07
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/src/main.rs
@@ -0,0 +1,334 @@
+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![None; iter.count()];
+
+ let iter = fs::read_dir(patches_directory)?;
+ 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");
+ }
+ let path = entry.path();
+ let idx: usize = {
+ let mut file_name = path
+ .file_name()
+ .expect("Should all have a filename")
+ .to_str()
+ .expect("Should be valid utf8")
+ .chars();
+ let number = file_name.clone().take(4).collect::<String>();
+ assert_eq!(file_name.nth(4), Some('-'));
+
+ number.parse().expect("Should parse as usize number")
+ };
+ output[idx - 1] = Some(path);
+ }
+ output.sort();
+ output
+ };
+ apply_patches(
+ patch_files.into_iter().map(|s| s.unwrap()).collect(),
+ 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(())
+}
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..ac30eaa4
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/src/parse.rs
@@ -0,0 +1,437 @@
+#![allow(clippy::no_effect)]
+
+use core::fmt;
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+
+use anyhow::{Result, bail};
+use pretty_assertions::assert_eq;
+
+#[derive(Clone)]
+pub(crate) struct Patch {
+ pub(crate) files: Vec<FileDiff>,
+ pub(crate) commit_message: String,
+ pub(crate) date: String,
+ pub(crate) final_whitespace: &'static str,
+ pub(crate) generating_version: 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)?;
+ }
+ writeln!(f, "--{}", self.final_whitespace)?;
+ writeln!(f, "{}", self.generating_version)?;
+
+ Ok(())
+ }
+}
+
+#[derive(Clone)]
+pub(crate) enum FileDiff {
+ Add {
+ path: PathBuf,
+ diff: String,
+ index: String,
+ new_file_mode: String,
+ },
+ Remove {
+ path: PathBuf,
+ diff: String,
+ index: String,
+ deleted_mode: String,
+ },
+ Change {
+ path: PathBuf,
+ diff: String,
+ index: String,
+ },
+ RenameNoChange {
+ from: PathBuf,
+ to: PathBuf,
+ },
+ RenameWithChange {
+ from: PathBuf,
+ to: PathBuf,
+ similarity: String,
+ diff: String,
+ index: String,
+ },
+}
+
+impl fmt::Display for FileDiff {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ FileDiff::Add {
+ path,
+ diff,
+ index,
+ new_file_mode,
+ } => {
+ writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?;
+ writeln!(f, "new file mode {}", new_file_mode)?;
+ writeln!(f, "index {}", index)?;
+ writeln!(f, "--- /dev/null")?;
+ writeln!(f, "+++ b/{}", path.display())?;
+ writeln!(f, "{}", diff)?;
+ }
+ FileDiff::Remove {
+ path,
+ diff,
+ index,
+ deleted_mode,
+ } => {
+ writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?;
+ writeln!(f, "deleted file mode {}", deleted_mode)?;
+ writeln!(f, "index {}", index)?;
+ writeln!(f, "--- a/{}", path.display())?;
+ writeln!(f, "+++ /dev/null")?;
+ writeln!(f, "{}", diff)?;
+ }
+ FileDiff::Change { path, diff, index } => {
+ writeln!(f, "diff --git a/{} b/{}", path.display(), path.display())?;
+ writeln!(f, "index {}", index)?;
+ writeln!(f, "--- a/{}", path.display())?;
+ writeln!(f, "+++ b/{}", path.display())?;
+ writeln!(f, "{}", diff)?;
+ }
+ FileDiff::RenameNoChange { from, to } => {
+ writeln!(f, "diff --git a/{} b/{}", from.display(), to.display())?;
+ writeln!(f, "similarity index 100%")?;
+ writeln!(f, "rename from {}", from.display())?;
+ writeln!(f, "rename to {}", to.display())?;
+ }
+ FileDiff::RenameWithChange {
+ from,
+ to,
+ similarity,
+ diff,
+ index,
+ } => {
+ writeln!(f, "diff --git a/{} b/{}", from.display(), to.display())?;
+ writeln!(f, "similarity index {}", similarity)?;
+ writeln!(f, "rename from {}", from.display())?;
+ writeln!(f, "rename to {}", 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;
+ }
+ };
+}
+
+macro_rules! parser {
+ (
+ $iterator:ident,
+ $debug:ident,
+ $($prefix:literal @ $prefix_var:ident => {$($expr:tt)*};)* $(;)?
+ .. $diff:ident
+ ) => {
+ $(
+ let $prefix_var = {
+ let next = $iterator.next().unwrap();
+ if $debug {
+ eprintln!(" {}", next);
+ }
+ next
+ }.strip_prefix($prefix).unwrap().to_owned();
+ $(
+ $expr
+ )*;
+ )*
+ let $diff = {
+ let mut diff = vec![];
+ let mut iter = $iterator.clone();
+ while let Some(next) = iter.next() {
+ if next.starts_with("diff --git") {
+ break;
+ }
+ if (next == "-- " || next == "--")
+ && iter.peek().map(|v| v.starts_with("2")) == Some(true)
+ {
+ break;
+ }
+ diff.push(next);
+ }
+ diff
+ };
+
+ for _ in 0..$diff.len() {
+ let next = $iterator.next().unwrap();
+ if $debug {
+ eprintln!(" {}", next);
+ }
+ }
+ let $diff = $diff.join("\n");
+
+ };
+}
+
+pub(crate) fn parse_file(file: &Path, debug: bool) -> Result<Patch> {
+ if debug {
+ eprintln!("Started parsing `{}`..", file.display());
+ }
+ let patch_text = fs::read_to_string(file)?;
+
+ let (commit_message, date, mut cut_idx) = {
+ let mut commit = None;
+ let mut date = None;
+ let mut author = None;
+ let mut final_message = None;
+ let mut finished_headers = false;
+ let mut cut_idx = None;
+ for (idx, line) in patch_text.lines().enumerate() {
+ 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()+2..].to_owned()
+ } else {
+ unreachable!("Email subjects always start with [PATCH x/y]");
+ }
+ });
+ if line.starts_with("MIME-Version: ")
+ || line.starts_with("Content-Type: ")
+ || line.starts_with("Content-Transfer-Encoding: ")
+ {
+ continue;
+ }
+
+ if line.starts_with(" ") {
+ // this line is a continuation of the previous header. We just assume
+ // it's the subject header.
+ final_message.as_mut().unwrap().push_str(line);
+ continue;
+ }
+
+ if line.is_empty() {
+ // add the newline to end the subject message
+ final_message.as_mut().unwrap().push('\n');
+ // and one newline to mark the end of the header and the start for more text.
+ final_message.as_mut().unwrap().push('\n');
+ finished_headers = true;
+ continue;
+ }
+
+ if line == "---" {
+ cut_idx = Some(idx);
+ break;
+ }
+
+ if finished_headers {
+ // If we finished all the headers, the rest must be commit message stuff.
+ // One per line.
+
+ final_message.as_mut().unwrap().push_str(line);
+ final_message.as_mut().unwrap().push('\n');
+ } else {
+ bail!("Failed to recognize commit message header: `{line}`, while parsing patch.")
+ }
+ }
+
+ if debug {
+ eprintln!("Patch data:");
+ eprintln!(" Author: {}", author.unwrap());
+ eprintln!(" Commit: {}", commit.unwrap());
+ eprintln!(" Date: {}", date.as_ref().unwrap());
+ for line in final_message.as_ref().unwrap().lines() {
+ eprintln!(" {}", line);
+ }
+ eprintln!("****************************");
+ }
+
+ (
+ final_message.unwrap().trim().to_owned(),
+ date.unwrap().trim().to_owned(),
+ cut_idx.unwrap(),
+ )
+ };
+
+ let mut patch_files = vec![];
+ let mut final_whitespace = None;
+ let mut generating_version = None;
+ let mut skipping_frontmatter = true;
+ let mut iterator = patch_text
+ .lines()
+ .skip(cut_idx)
+ // skip one more to go over the `---` line
+ .skip(1)
+ .peekable();
+ while let Some(line) = iterator.peek() {
+ if debug {
+ eprintln!("{}", line);
+ }
+
+ if (line.starts_with(" ") || line.is_empty()) && skipping_frontmatter {
+ iterator.next().unwrap();
+ cut_idx += 1;
+ continue;
+ }
+
+ if line.starts_with("diff --git") {
+ if skipping_frontmatter {
+ skipping_frontmatter = false;
+ }
+ iterator.next().unwrap();
+ continue;
+ }
+
+ match line {
+ line if line.starts_with("new file mode") => {
+ parser! {
+ iterator, debug,
+ "new file mode " @ mode => {let new_file_mode = mode.to_owned()};
+ "index " @ index => {let index = index.to_owned()};
+ "--- " @ from => {assert_eq!(from, "/dev/null")};
+ "+++ b/" @ to => {let path = PathBuf::from(to)};
+ ..diff
+ }
+ patch_files.push(FileDiff::Add {
+ path,
+ diff,
+ index,
+ new_file_mode,
+ })
+ }
+ line if line.starts_with("index") => {
+ parser! {
+ iterator, debug,
+ "index " @ index => {()};
+ "--- a/" @ from => {()};
+ "+++ b/" @ to => {assert_eq!(from, to); let path = PathBuf::from(to)};
+ ..diff
+ }
+ patch_files.push(FileDiff::Change { path, diff, index })
+ }
+ line if line.starts_with("similarity index 100%") => {
+ parser! {
+ iterator, debug,
+ "similarity index 100%" @ none => {assert!(none.is_empty())};
+ "rename from " @ from => {let from = PathBuf::from(from)};
+ "rename to " @ to => {let to = PathBuf::from(to)};
+ .. no_diff
+ }
+ assert!(no_diff.is_empty());
+
+ patch_files.push(FileDiff::RenameNoChange { from, to })
+ }
+ line if line.starts_with("similarity index ") => {
+ parser! {
+ iterator, debug,
+ "similarity index " @ similarity => {()};
+ "rename from " @ from_1 => {let from = PathBuf::from(&from_1)};
+ "rename to " @ to_1 => {let to = PathBuf::from(&to_1)};
+ "index " @ index => {()};
+ "--- a/" @ from_2 => {assert_eq!(from_1, from_2)};
+ "+++ b/" @ to_2 => {assert_eq!(to_1, to_2)};
+ .. diff
+ }
+
+ patch_files.push(FileDiff::RenameWithChange {
+ from,
+ to,
+ similarity,
+ diff,
+ index,
+ })
+ }
+ line if line.starts_with("deleted") => {
+ parser! {
+ iterator, debug,
+ "deleted file mode " @ deleted_mode => {()};
+ "index " @ index => {()};
+ "--- a/" @ from => {let path = PathBuf::from(from)};
+ "+++ /dev/null" @ rest => {assert!(rest.is_empty())};
+ ..diff
+ }
+ patch_files.push(FileDiff::Remove {
+ path,
+ diff,
+ index,
+ deleted_mode,
+ })
+ }
+ line if *line == "--" || *line == "-- " => {
+ final_whitespace = Some( match *line {
+ "-- " => " ",
+ "--" => "",
+ _ => unreachable!(),
+ });
+ iterator.next().unwrap();
+ generating_version = Some(iterator.next().unwrap().to_owned());
+ assert_eq!(iterator.next(), Some(""));
+ assert_eq!(iterator.next(), None);
+ break;
+ }
+ _ => {
+ unreachable!("All possible line starts should be covered, but: `{line}` is not.")
+ }
+ }
+ }
+
+ let patch = Patch {
+ files: patch_files,
+ commit_message,
+ date,
+ final_whitespace: final_whitespace.unwrap(),
+ generating_version: generating_version.unwrap(),
+ };
+
+ let patch_text = patch_text
+ .lines()
+ .skip(cut_idx)
+ // skip one more to go over the `---` line
+ .skip(1)
+ .collect::<Vec<_>>()
+ .join("\n");
+ let check_content = patch.to_string();
+
+ assert_eq!(
+ check_content, patch_text,
+ "Expected parsed patchtext to be the same as our the original patch"
+ );
+
+ Ok(patch)
+}
diff --git a/pkgs/by-name/ro/rollup/update.sh b/pkgs/by-name/ro/rollup/update.sh
new file mode 100755
index 00000000..5aedf3b3
--- /dev/null
+++ b/pkgs/by-name/ro/rollup/update.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env sh
+
+# Mpdpopm - A mpd rating tracker
+#
+# Copyright (C) 2026 Benedikt Peetz, Michael Herstine <sp1ff@pobox.com> <benedikt.peetz@b-peetz.de, sp1ff@pobox.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
+# This file is part of Mpdpopm.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+[ "$1" = "upgrade" ] && cargo upgrade --incompatible allow --pinned allow --recursive true
+cargo update --recursive