aeruginous 3.0.5

The Aeruginous Open Source Development Toolbox.
Documentation
/*********************** GNU General Public License 3.0 ***********************\
|                                                                              |
|  Copyright (C) 2023 Kevin Matthes                                            |
|                                                                              |
|  This program is free software: you can redistribute it and/or modify        |
|  it under the terms of the GNU General Public License as published by        |
|  the Free Software Foundation, either version 3 of the License, or           |
|  (at your option) any later version.                                         |
|                                                                              |
|  This program is distributed in the hope that it will be useful,             |
|  but WITHOUT ANY WARRANTY; without even the implied warranty of              |
|  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               |
|  GNU General Public License for more details.                                |
|                                                                              |
|  You should have received a copy of the GNU General Public License           |
|  along with this program.  If not, see <https://www.gnu.org/licenses/>.      |
|                                                                              |
\******************************************************************************/

use crate::{
    ceprintlns, AppendAsLine, PatternWriter, ReadFile, Version, VersionRange,
};
use std::{path::PathBuf, str::FromStr};
use sysexits::{ExitCode, Result};

/// Increment a hard-coded version string in some files.
#[derive(clap::Parser, Clone)]
#[command(visible_aliases = ["incver", "inc-ver", "incrementversion"])]
pub struct IncrementVersion {
    /// The files to work on.
    #[arg(long = "edit", short = 'e')]
    file_to_edit: Vec<PathBuf>,

    /// The files to update and reformat.
    #[arg(long = "rewrite", short = 'R')]
    file_to_rewrite: Vec<PathBuf>,

    /// The old version to search for and replace.
    #[arg(long, short = 'v')]
    old_version: String,

    /// In case of Rust projects:  which package's version shall be edited?
    #[arg(long, short)]
    package: Option<String>,

    /// The increment range.
    #[arg(long, short)]
    range: VersionRange,
}

impl IncrementVersion {
    /// Process the input data.
    ///
    /// # Errors
    ///
    /// See
    ///
    /// - [`PatternWriter::truncate`]
    /// - [`ReadFile::read`]
    /// - [`Version::from_str`]
    /// - [`sysexits::ExitCode::DataErr`]
    /// - [`sysexits::ExitCode::Unavailable`]
    /// - [`sysexits::ExitCode::Usage`]
    pub fn main(&self) -> Result<()> {
        self.wrap().main()
    }

    /// Construct a new instance.
    #[must_use]
    pub fn new(
        file_to_edit: Vec<PathBuf>,
        file_to_rewrite: Vec<PathBuf>,
        old_version: String,
        package: Option<String>,
        range: VersionRange,
    ) -> Self {
        Self {
            file_to_edit,
            file_to_rewrite,
            old_version,
            package,
            range,
        }
    }

    fn wrap(&self) -> Logic {
        Logic {
            cli: self.clone(),
            new_version: String::new(),
            old_version: self
                .old_version
                .strip_prefix('v')
                .unwrap_or(&self.old_version)
                .to_string(),
        }
    }
}

struct Logic {
    cli: IncrementVersion,
    new_version: String,
    old_version: String,
}

impl Logic {
    fn determine_new_version(&mut self) -> Result<()> {
        self.new_version = Version::from_str(&self.old_version)?
            .increment(self.cli.range)
            .to_string();
        self.new_version = self
            .new_version
            .strip_prefix('v')
            .unwrap_or(&self.new_version)
            .to_string();
        Ok(())
    }

    fn edit_cargo_lock(&self, file: &PathBuf) -> Result<()> {
        if let Some(rust_package) = &self.cli.package {
            let mut edited = false;
            let mut lock_file = match cargo_lock::Lockfile::load(file) {
                Ok(l) => Ok(l),
                Err(cargo_lock::Error::Io(e)) => Err(e.into()),
                Err(_) => Err(ExitCode::Unavailable),
            }?;

            for package in &mut lock_file.packages {
                if package.name.as_str() == rust_package {
                    match self.cli.range {
                        VersionRange::Major => {
                            package.version.major += 1;
                            package.version.minor = 0;
                            package.version.patch = 0;
                        }
                        VersionRange::Minor => {
                            package.version.minor += 1;
                            package.version.patch = 0;
                        }
                        VersionRange::Patch => package.version.patch += 1,
                    }

                    edited = true;
                    break;
                }
            }

            if edited {
                file.truncate(Box::new(lock_file.clone().to_string()))
            } else {
                ceprintlns!("Package"!Red, "not found.");
                Err(ExitCode::DataErr)
            }
        } else {
            ceprintlns!("Package"!Red, "not specified.");
            Err(ExitCode::Usage)
        }
    }

    fn edit_cargo_toml(&self, file: &PathBuf) -> Result<()> {
        let mut buffer = String::new();
        let mut package_reached = false;
        let mut package_updated = false;

        for line in file.read()?.lines() {
            if line.starts_with("[package]") {
                package_reached = true;
            }

            if line.starts_with("version")
                && line.contains(&self.old_version)
                && package_reached
                && !package_updated
            {
                package_updated = true;
                buffer.append_as_line(
                    line.replace(&self.old_version, &self.new_version),
                );
            } else {
                buffer.append_as_line(line);
            }
        }

        file.truncate(Box::new(buffer))
    }

    fn edit_citation_cff(&self, file: &PathBuf) -> Result<()> {
        let mut buffer = String::new();

        for line in file.read()?.lines() {
            if line.starts_with("version:") {
                buffer.append_as_line(
                    line.replace(&self.old_version, &self.new_version),
                );
            } else {
                buffer.append_as_line(line);
            }
        }

        file.truncate(Box::new(buffer))
    }

    fn edit_normal_file(&self, file: &PathBuf) -> Result<()> {
        file.truncate(Box::new(
            file.read()?.replace(&self.old_version, &self.new_version),
        ))
    }

    fn main(&mut self) -> Result<()> {
        self.determine_new_version()?;

        for file in &self.cli.file_to_edit {
            match file
                .file_name()
                .ok_or(ExitCode::Usage)?
                .to_str()
                .ok_or(ExitCode::DataErr)?
            {
                "Cargo.lock" => self.edit_cargo_lock(file)?,
                "Cargo.toml" => self.edit_cargo_toml(file)?,
                "CITATION.cff" => self.edit_citation_cff(file)?,
                _ => self.edit_normal_file(file)?,
            }
        }

        for file in &self.cli.file_to_rewrite {
            match file
                .file_name()
                .ok_or(ExitCode::Usage)?
                .to_str()
                .ok_or(ExitCode::DataErr)?
            {
                "Cargo.lock" => self.edit_cargo_lock(file)?,
                "Cargo.toml" => self.rewrite_cargo_toml(file)?,
                "CITATION.cff" => self.edit_citation_cff(file)?,
                _ => self.edit_normal_file(file)?,
            }
        }

        Ok(())
    }

    fn rewrite_cargo_toml(&self, file: &PathBuf) -> Result<()> {
        if let Ok(mut manifest) = file.read()?.parse::<toml::Table>() {
            if manifest["package"]["version"].as_str().is_some() {
                manifest["package"]["version"] =
                    self.new_version.clone().into();
                file.truncate(Box::new(manifest.to_string()))?;

                Ok(())
            } else {
                ceprintlns!(
                  "Cargo.toml"!Red,
                  "does not contain a `package.version` field."
                );

                Err(ExitCode::DataErr)
            }
        } else {
            ceprintlns!("Cargo.toml"!Red, "does not seem to be valid TOML.");
            Err(ExitCode::DataErr)
        }
    }
}

/******************************************************************************/