diff options
Diffstat (limited to 'pkgs/by-name/ro')
| -rw-r--r-- | pkgs/by-name/ro/rollup/.envrc | 21 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/.gitignore | 16 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/Cargo.lock | 216 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/Cargo.toml | 21 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/package.unstable.nix | 40 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/main.rs | 334 | ||||
| -rw-r--r-- | pkgs/by-name/ro/rollup/src/parse.rs | 437 | ||||
| -rwxr-xr-x | pkgs/by-name/ro/rollup/update.sh | 14 |
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 |
