From d70df55f899a9f264526f5157b66d2ab82a4ed78 Mon Sep 17 00:00:00 2001 From: Jo <10510431+j178@users.noreply.github.com> Date: Sat, 16 Nov 2024 22:29:38 +0800 Subject: [PATCH] Add `pre-commit self udpate` (#68) --- Cargo.lock | 1 + Cargo.toml | 1 + src/cli/mod.rs | 30 +++++++ src/cli/self_update.rs | 175 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 ++- 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/cli/self_update.rs diff --git a/Cargo.lock b/Cargo.lock index 9c1e057..0fe17ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1282,6 +1282,7 @@ dependencies = [ "fs2", "futures", "home", + "http", "indicatif", "indoc", "insta", diff --git a/Cargo.toml b/Cargo.toml index a66e18f..d47d292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ fs-err = "2.11.0" fs2 = "0.4.3" futures = "0.3.31" home = "0.5.9" +http = "1.1.0" indicatif = "0.17.8" indoc = "2.0.5" itertools = "0.13.0" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d28ffd4..aedff20 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -11,6 +11,7 @@ mod hook_impl; mod install; mod run; mod sample_config; +mod self_update; mod validate; pub(crate) use clean::clean; @@ -18,6 +19,7 @@ pub(crate) use hook_impl::hook_impl; pub(crate) use install::{install, uninstall}; pub(crate) use run::run; pub(crate) use sample_config::sample_config; +pub(crate) use self_update::self_update; pub(crate) use validate::{validate_configs, validate_manifest}; #[derive(Copy, Clone)] @@ -178,6 +180,10 @@ pub(crate) enum Command { #[command(hide = true)] HookImpl(HookImplArgs), + /// `pre-commit-rs` self management. + #[command(name = "self")] + Self_(SelfNamespace), + /// Generate shell completion scripts. #[command(hide = true)] GenerateShellCompletion(GenerateShellCompletionArgs), @@ -303,6 +309,30 @@ pub(crate) struct HookImplArgs { pub(crate) args: Vec, } +#[derive(Debug, Args)] +pub struct SelfNamespace { + #[command(subcommand)] + pub command: SelfCommand, +} + +#[derive(Debug, Subcommand)] +pub enum SelfCommand { + /// Update pre-commit-rs. + Update(SelfUpdateArgs), +} + +#[derive(Debug, Args)] +pub struct SelfUpdateArgs { + /// Update to the specified version. + /// If not provided, pre-commit-rs will update to the latest version. + pub target_version: Option, + + /// A GitHub token for authentication. + /// A token is not required but can be used to reduce the chance of encountering rate limits. + #[arg(long, env = "GITHUB_TOKEN")] + pub token: Option, +} + #[derive(Debug, Args)] pub(crate) struct GenerateShellCompletionArgs { /// The shell to generate the completion script for diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs new file mode 100644 index 0000000..8838e53 --- /dev/null +++ b/src/cli/self_update.rs @@ -0,0 +1,175 @@ +// MIT License +// +// Copyright (c) 2023 Astral Software Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use std::fmt::Write; + +use anyhow::Result; +use axoupdater::{AxoUpdater, AxoupdateError, UpdateRequest}; +use owo_colors::OwoColorize; +use tracing::debug; + +use crate::cli::ExitStatus; +use crate::printer::Printer; + +/// Attempt to update the pre-commit-rs binary. +pub(crate) async fn self_update( + version: Option, + token: Option, + printer: Printer, +) -> Result { + let mut updater = AxoUpdater::new_for("pre-commit-rs"); + updater.disable_installer_output(); + + if let Some(ref token) = token { + updater.set_github_token(token); + } + + // Load the "install receipt" for the current binary. If the receipt is not found, then + // pre-commit-rs was likely installed via a package manager. + let Ok(updater) = updater.load_receipt() else { + debug!("no receipt found; assuming pre-commit-rs was installed via a package manager"); + writeln!( + printer.stderr(), + "{}", + format_args!( + concat!( + "{}{} Self-update is only available for pre-commit-rs binaries installed via the standalone installation scripts.", + "\n", + "\n", + "If you installed pre-commit-rs with pip, brew, or another package manager, update pre-commit-rs with `pip install --upgrade`, `brew upgrade`, or similar." + ), + "warning".yellow().bold(), + ":".bold() + ) + )?; + return Ok(ExitStatus::Error); + }; + + // Ensure the receipt is for the current binary. If it's not, then the user likely has multiple + // pre-commit-rs binaries installed, and the current binary was _not_ installed via the standalone + // installation scripts. + if !updater.check_receipt_is_for_this_executable()? { + debug!( + "receipt is not for this executable; assuming pre-commit-rs was installed via a package manager" + ); + writeln!( + printer.stderr(), + "{}", + format_args!( + concat!( + "{}{} Self-update is only available for pre-commit-rs binaries installed via the standalone installation scripts.", + "\n", + "\n", + "If you installed pre-commit-rs with pip, brew, or another package manager, update pre-commit-rs with `pip install --upgrade`, `brew upgrade`, or similar." + ), + "warning".yellow().bold(), + ":".bold() + ) + )?; + return Ok(ExitStatus::Error); + } + + writeln!( + printer.stderr(), + "{}", + format_args!( + "{}{} Checking for updates...", + "info".cyan().bold(), + ":".bold() + ) + )?; + + let update_request = if let Some(version) = version { + UpdateRequest::SpecificTag(version) + } else { + UpdateRequest::Latest + }; + + updater.configure_version_specifier(update_request); + + // Run the updater. This involves a network request, since we need to determine the latest + // available version of pre-commit-rs. + match updater.run().await { + Ok(Some(result)) => { + let version_information = if let Some(old_version) = result.old_version { + format!( + "from {} to {}", + format!("v{old_version}").bold().white(), + format!("v{}", result.new_version).bold().white(), + ) + } else { + format!("to {}", format!("v{}", result.new_version).bold().white()) + }; + + writeln!( + printer.stderr(), + "{}", + format_args!( + "{}{} Upgraded pre-commit-rs {}! {}", + "success".green().bold(), + ":".bold(), + version_information, + format!( + "https://github.com/j178/pre-commit-rs/releases/tag/{}", + result.new_version_tag + ) + .cyan() + ) + )?; + } + Ok(None) => { + writeln!( + printer.stderr(), + "{}", + format_args!( + "{}{} You're on the latest version of pre-commit-rs ({})", + "success".green().bold(), + ":".bold(), + format!("v{}", env!("CARGO_PKG_VERSION")).bold().white() + ) + )?; + } + Err(err) => { + return if let AxoupdateError::Reqwest(err) = err { + if err.status() == Some(http::StatusCode::FORBIDDEN) && token.is_none() { + writeln!( + printer.stderr(), + "{}", + format_args!( + "{}{} GitHub API rate limit exceeded. Please provide a GitHub token via the {} option.", + "error".red().bold(), + ":".bold(), + "`--token`".green().bold() + ) + )?; + Ok(ExitStatus::Error) + } else { + Err(err.into()) + } + } else { + Err(err.into()) + }; + } + } + + Ok(ExitStatus::Success) +} diff --git a/src/main.rs b/src/main.rs index 4c0c86b..6a5d45e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use tracing::{debug, error}; use tracing_subscriber::filter::Directive; use tracing_subscriber::EnvFilter; -use crate::cli::{Cli, Command, ExitStatus}; +use crate::cli::{Cli, Command, ExitStatus, SelfCommand, SelfNamespace, SelfUpdateArgs}; use crate::git::get_root; use crate::printer::Printer; @@ -218,6 +218,13 @@ async fn run(mut cli: Cli) -> Result { Ok(cli::validate_manifest(args.manifests)) } Command::SampleConfig => Ok(cli::sample_config()), + Command::Self_(SelfNamespace { + command: + SelfCommand::Update(SelfUpdateArgs { + target_version, + token, + }), + }) => cli::self_update(target_version, token, printer).await, Command::GenerateShellCompletion(args) => { show_settings!(args);