use anyhow::{Context, Result};
use std::io::IsTerminal;
const REPO_OWNER: &str = "hiro-o918";
const REPO_NAME: &str = "rinkaku";
const BIN_NAME: &str = "rinkaku";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConfirmMode {
Skip,
Prompt,
RefuseNonInteractive,
}
fn confirm_mode(yes: bool, stdin_is_tty: bool) -> ConfirmMode {
if yes {
ConfirmMode::Skip
} else if stdin_is_tty {
ConfirmMode::Prompt
} else {
ConfirmMode::RefuseNonInteractive
}
}
fn is_affirmative(answer: &str) -> bool {
matches!(answer.trim().to_lowercase().as_str(), "y" | "yes")
}
pub fn run_self_update(yes: bool) -> Result<()> {
let confirm_mode = confirm_mode(yes, std::io::stdin().is_terminal());
if confirm_mode == ConfirmMode::RefuseNonInteractive {
anyhow::bail!("refusing to self-update non-interactively; pass --yes to proceed");
}
let current_version = env!("CARGO_PKG_VERSION");
let target = self_update::get_target();
let updater = self_update::backends::github::Update::configure()
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.bin_name(BIN_NAME)
.target(target)
.current_version(current_version)
.identifier(&release_asset_name(BIN_NAME, target))
.bin_path_in_archive(&archive_bin_path(BIN_NAME, target))
.no_confirm(true)
.show_output(false)
.show_download_progress(true)
.build()?;
let latest = updater
.get_latest_release()
.context("failed to check the latest rinkaku release")?;
let is_newer = self_update::version::bump_is_greater(current_version, &latest.version)
.with_context(|| {
format!(
"failed to compare current version v{current_version} with latest v{}",
latest.version
)
})?;
if !is_newer {
println!("{BIN_NAME} is already up to date (v{current_version})");
return Ok(());
}
println!(
"New release found: v{current_version} -> v{}",
latest.version
);
if confirm_mode == ConfirmMode::Prompt {
print!("Update to v{}? [y/N]: ", latest.version);
std::io::Write::flush(&mut std::io::stdout())?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if !is_affirmative(&answer) {
println!("Update cancelled");
return Ok(());
}
}
let status = updater.update().context(
"self-update failed; if this is a permission error, try again with sudo, \
or update via the package manager you installed rinkaku with \
(e.g. `brew upgrade` or `cargo install rinkaku`)",
)?;
if status.updated() {
println!(
"Updated {BIN_NAME} from v{current_version} to {}",
status.version()
);
} else {
println!("{BIN_NAME} is already up to date (v{current_version})");
}
Ok(())
}
fn release_asset_name(bin: &str, target: &str) -> String {
format!("{bin}-{target}.tar.gz")
}
fn archive_bin_path(bin: &str, target: &str) -> String {
format!("{bin}-{target}/{bin}")
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn should_build_dot_tar_gz_asset_name_from_bin_and_target() {
let actual = release_asset_name("rinkaku", "aarch64-apple-darwin");
assert_eq!("rinkaku-aarch64-apple-darwin.tar.gz", actual);
}
#[test]
fn should_build_nested_archive_path_from_bin_and_target() {
let actual = archive_bin_path("rinkaku", "x86_64-unknown-linux-gnu");
assert_eq!("rinkaku-x86_64-unknown-linux-gnu/rinkaku", actual);
}
#[test]
fn should_configure_update_builder_without_error() {
let target = self_update::get_target();
let current_version = env!("CARGO_PKG_VERSION");
let result = self_update::backends::github::Update::configure()
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.bin_name(BIN_NAME)
.target(target)
.current_version(current_version)
.identifier(&release_asset_name(BIN_NAME, target))
.bin_path_in_archive(&archive_bin_path(BIN_NAME, target))
.no_confirm(true)
.show_output(false)
.show_download_progress(true)
.build();
assert!(
result.is_ok(),
"Update builder configuration should succeed"
);
}
#[test]
fn should_skip_confirmation_when_yes_flag_is_set() {
let actual = confirm_mode(true, false);
assert_eq!(ConfirmMode::Skip, actual);
}
#[test]
fn should_prompt_when_yes_flag_is_unset_and_stdin_is_a_tty() {
let actual = confirm_mode(false, true);
assert_eq!(ConfirmMode::Prompt, actual);
}
#[test]
fn should_refuse_non_interactively_when_yes_flag_is_unset_and_stdin_is_not_a_tty() {
let actual = confirm_mode(false, false);
assert_eq!(ConfirmMode::RefuseNonInteractive, actual);
}
#[test]
fn should_skip_confirmation_when_yes_flag_is_set_even_with_a_tty() {
let actual = confirm_mode(true, true);
assert_eq!(ConfirmMode::Skip, actual);
}
#[rstest]
#[case::should_accept_lowercase_y("y", true)]
#[case::should_accept_uppercase_y("Y", true)]
#[case::should_accept_lowercase_yes("yes", true)]
#[case::should_accept_uppercase_yes("YES", true)]
#[case::should_accept_mixed_case_yes("Yes", true)]
#[case::should_accept_y_with_surrounding_whitespace(" y \n", true)]
#[case::should_accept_yes_with_surrounding_whitespace(" yes \n", true)]
#[case::should_reject_empty_string("", false)]
#[case::should_reject_whitespace_only_string(" \n", false)]
#[case::should_reject_n("n", false)]
#[case::should_reject_no("no", false)]
#[case::should_reject_arbitrary_text("sure why not", false)]
#[case::should_reject_y_as_prefix_of_longer_word("yesterday", false)]
fn should_check_is_affirmative(#[case] answer: &str, #[case] expected: bool) {
let actual = is_affirmative(answer);
assert_eq!(expected, actual);
}
}