Skip to content

Commit

Permalink
feat: inform users of breaking changes on first run (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveLauC authored Dec 3, 2023
1 parent 18b37ce commit 788e041
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 5 deletions.
Empty file added BREAKINGCHANGES.md
Empty file.
12 changes: 12 additions & 0 deletions BREAKINGCHNAGES_dev.md
Original file line number Diff line number Diff line change
@@ -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 = ""
```
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade"
version = "13.0.0"
authors = ["Roey Darwish Dror <[email protected]>", "Thomas Schönauer <[email protected]>"]
exclude = ["doc/screenshot.gif"]
exclude = ["doc/screenshot.gif", "BREAKINGCHNAGES_dev.md"]
edition = "2021"

readme = "README.md"
Expand Down
156 changes: 156 additions & 0 deletions src/breaking_changes.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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()?;
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 788e041

Please sign in to comment.