feat: inform users of breaking changes on first run (#619)
This commit is contained in:
0
BREAKINGCHANGES.md
Normal file
0
BREAKINGCHANGES.md
Normal file
12
BREAKINGCHNAGES_dev.md
Normal file
12
BREAKINGCHNAGES_dev.md
Normal 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 = ""
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
@@ -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
156
src/breaking_changes.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
25
src/main.rs
25
src/main.rs
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user