topgrade 17.4.0

Upgrade all the things
//! Inform the users of the breaking changes introduced in this major release.
//!
//! Print the breaking changes and possibly a migration guide when:
//!     1. The Topgrade being executed is a new major release
//!     2. This is the first launch of that major release

#[cfg(windows)]
use crate::WINDOWS_DIRS;
#[cfg(unix)]
use crate::XDG_DIRS;
use crate::terminal::{print_separator, prompt_yesno};
use color_eyre::eyre::{Context, Result};
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
use semver::Version;
use std::path::PathBuf;
use std::process::exit;
use std::{env, fs};

/// Version string x.y.z
static VERSION_STR: &str = env!("CARGO_PKG_VERSION");

/// Topgrade's breaking changes
///
/// We store them in the compiled binary. They are generated by build.rs.
static BREAKINGCHANGES: &str = include_str!(concat!(env!("OUT_DIR"), "/breaking_changes.txt"));

pub(crate) fn run() -> Result<()> {
    let version = VERSION_STR.parse::<Version>().expect("should be a valid version");
    let keep_file = keep_file_path();

    // This is the first run of Topgrade, or the first run of Topgrade v17 (which added the current
    //  breaking changes functionality)
    if !keep_file.exists() {
        // If this is the first run of Topgrade, there's no need to notify the user about any changes
        // If this is the first run of Topgrade v17, there are no breaking changes. We will
        //  assume every user runs Topgrade v17 before running Topgrade v18, causing a 17.x.x
        //  version to be written to the keep file, causing v18 to trigger the breaking changes notification.
        write_keep_file()?;
        return Ok(());
    }

    // If the major version is higher than the major part of the last ran version
    //  (this does only show v3 release notes if upgrading from v1 to v3 (skipping v2), but
    //  that isn't going to happen a lot anyway, and this is a lot simpler.)
    if version.major
        > fs::read_to_string(&keep_file)?
            .parse::<Version>()
            .wrap_err_with(|| format!("Invalid version in Topgrade keep file at {}", keep_file.display()))?
            .major
    {
        print_separator(t!(
            "Topgrade {version_str} Breaking Changes",
            version_str = format!("v{}", version.major)
        ));
        let contents = if BREAKINGCHANGES.is_empty() {
            t!("No breaking changes").to_string()
        } else {
            BREAKINGCHANGES.to_string()
        };
        println!(
            "{contents}\nSee the full release notes at {}\n",
            release_notes_link(VERSION_STR)
        );

        if prompt_yesno(&t!("Continue?"))? {
            write_keep_file()?;
        } else {
            exit(1);
        }
    }

    Ok(())
}

fn release_notes_link(version: &str) -> String {
    format!("https://github.com/topgrade-rs/topgrade/releases/tag/v{version}")
}

fn write_keep_file() -> Result<()> {
    fs::create_dir_all(data_dir())?;
    fs::write(keep_file_path(), VERSION_STR)?;
    Ok(())
}

/// Return platform's data directory.
fn data_dir() -> PathBuf {
    #[cfg(unix)]
    return XDG_DIRS.data_dir();

    #[cfg(windows)]
    return WINDOWS_DIRS.data_dir();
}

/// Return Topgrade's keep file path.
///
/// keep file is a file under the data directory containing a major version
/// number, it will be created on first run and is used to check if an execution
/// of Topgrade is the first run of a major release, for more details, see
/// `first_run_of_major_release()`.
fn keep_file_path() -> PathBuf {
    data_dir().join("topgrade_keep")
}

/// If environment variable `TOPGRADE_SKIP_BRKC_NOTIFY` is set to `true`, then
/// we won't notify the user of the breaking changes.
pub(crate) fn should_skip() -> bool {
    env::var("TOPGRADE_SKIP_BRKC_NOTIFY").is_ok_and(|var| var.as_str() == "true")
}