feat: inform users of breaking changes on first run (#619)

This commit is contained in:
SteveLauC
2023-12-03 09:52:35 +08:00
committed by GitHub
parent 18b37ce3e3
commit 788e0412f6
6 changed files with 203 additions and 5 deletions

0
BREAKINGCHANGES.md Normal file
View File

12
BREAKINGCHNAGES_dev.md Normal file
View File

@@ -0,0 +1,12 @@
1. In 13.0.0, we introduced a new feature, pushing git repos, now this feature
has been removed as some users are not satisfied with it.
For configuration entries, the following ones are gone:
```toml
[git]
pull_only_repos = []
push_only_repos = []
pull_arguments = ""
push_arguments = ""
```

View File

@@ -101,6 +101,19 @@ Be sure to apply your changes to
[`config.example.toml`](https://github.com/topgrade-rs/topgrade/blob/master/config.example.toml),
and have some basic documentations guiding user how to use these options.
## Breaking changes
If your PR introduces a breaking change, document it in `BREAKINGCHANGE_dev.md`,
it should be written in Markdown and wrapped in 80, for example:
```md
1. The configuration location has been updated to x.
2. The step x has been removed.
3. ...
```
## Before you submit your PR
Make sure your patch passes the following tests on your host:

View File

@@ -7,7 +7,7 @@ license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade"
version = "13.0.0"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif"]
exclude = ["doc/screenshot.gif", "BREAKINGCHNAGES_dev.md"]
edition = "2021"
readme = "README.md"

156
src/breaking_changes.rs Normal file
View File

@@ -0,0 +1,156 @@
//! 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
use crate::terminal::print_separator;
#[cfg(windows)]
use crate::WINDOWS_DIRS;
#[cfg(unix)]
use crate::XDG_DIRS;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use std::{
fs::{read_to_string, OpenOptions},
io::Write,
path::PathBuf,
str::FromStr,
};
/// Version string x.y.z
static VERSION_STR: &str = env!("CARGO_PKG_VERSION");
/// Version info
#[derive(Debug)]
pub(crate) struct Version {
_major: u64,
minor: u64,
patch: u64,
}
impl FromStr for Version {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
const NOT_SEMVER: &str = "Topgrade version is not semantic";
const NOT_NUMBER: &str = "Topgrade version is not dot-separated numbers";
let mut iter = s.split('.').take(3);
let major = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
let minor = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
let patch = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
// They cannot be all 0s
assert!(
!(major == 0 && minor == 0 && patch == 0),
"Version numbers can not be all 0s"
);
Ok(Self {
_major: major,
minor,
patch,
})
}
}
impl Version {
/// True if this version is a new major release.
pub(crate) fn is_new_major_release(&self) -> bool {
// We have already checked that they cannot all be zeros, so `self.major`
// is guaranteed to be non-zero.
self.minor == 0 && self.patch == 0
}
}
/// Topgrade's breaking changes
///
/// We store them in the compiled binary.
pub(crate) static BREAKINGCHANGES: &str = include_str!("../BREAKINGCHANGES.md");
/// 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 {
let keep_file = "topgrade_keep";
data_dir().join(keep_file)
}
/// True if this is the first execution of a major release.
pub(crate) fn first_run_of_major_release() -> Result<bool> {
let version = VERSION_STR.parse::<Version>().expect("should be a valid version");
let keep_file = keep_file_path();
// disable this lint here as the current code has better readability
#[allow(clippy::collapsible_if)]
if version.is_new_major_release() {
if !keep_file.exists() || read_to_string(&keep_file)? != VERSION_STR {
return Ok(true);
}
}
Ok(false)
}
/// Print breaking changes to the user.
pub(crate) fn print_breaking_changes() {
let header = format!("Topgrade {VERSION_STR} Breaking Changes");
print_separator(header);
let contents = if BREAKINGCHANGES.is_empty() {
"No Breaking changes"
} else {
BREAKINGCHANGES
};
println!("{contents}\n");
}
/// This function will be ONLY executed when the user has confirmed the breaking
/// changes, once confirmed, we write the keep file, which means the first run
/// of this major release is finished.
pub(crate) fn write_keep_file() -> Result<()> {
std::fs::create_dir_all(data_dir())?;
let keep_file = keep_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(keep_file)?;
let _ = file.write(VERSION_STR.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn is_new_major_release_works() {
let first_major_release: Version = "1.0.0".parse().unwrap();
let under_dev: Version = "0.1.0".parse().unwrap();
assert!(first_major_release.is_new_major_release());
assert!(!under_dev.is_new_major_release());
}
#[test]
#[should_panic(expected = "Version numbers can not be all 0s")]
fn invalid_version() {
let all_0 = "0.0.0";
all_0.parse::<Version>().unwrap();
}
}

View File

@@ -6,14 +6,17 @@ use std::path::PathBuf;
use std::process::exit;
use std::time::Duration;
use crate::breaking_changes::{first_run_of_major_release, print_breaking_changes, write_keep_file};
use clap::CommandFactory;
use clap::{crate_version, Parser};
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use console::Key;
use etcetera::base_strategy::BaseStrategy;
#[cfg(windows)]
use etcetera::base_strategy::Windows;
use etcetera::base_strategy::{BaseStrategy, Xdg};
#[cfg(unix)]
use etcetera::base_strategy::Xdg;
use once_cell::sync::Lazy;
use tracing::debug;
@@ -26,6 +29,7 @@ use self::terminal::*;
use self::utils::{install_color_eyre, install_tracing, update_tracing};
mod breaking_changes;
mod command;
mod config;
mod ctrlc;
@@ -43,10 +47,11 @@ mod sudo;
mod terminal;
mod utils;
pub static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
pub static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
pub(crate) static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
#[cfg(unix)]
pub(crate) static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
#[cfg(windows)]
pub static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
pub(crate) static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
fn run() -> Result<()> {
install_color_eyre()?;
@@ -130,6 +135,18 @@ fn run() -> Result<()> {
let ctx = execution_context::ExecutionContext::new(run_type, sudo, &git, &config);
let mut runner = runner::Runner::new(&ctx);
// If this is the first execution of a major release, inform user of breaking
// changes
if first_run_of_major_release()? {
print_breaking_changes();
if prompt_yesno("Confirmed?")? {
write_keep_file()?;
} else {
exit(1);
}
}
// Self-Update step, this will execute only if:
// 1. the `self-update` feature is enabled
// 2. it is not disabled from configuration (env var/CLI opt/file)