mirror of
https://github.com/ProvableHQ/leo.git
synced 2024-11-28 01:01:53 +03:00
Merge pull request #344 from AleoHQ/feature/config-automatic
Automatic update configurations
This commit is contained in:
commit
02f9aa8dee
28
leo/cli.rs
28
leo/cli.rs
@ -33,11 +33,17 @@ pub trait CLI {
|
||||
fn new<'a, 'b>() -> App<'a, 'b> {
|
||||
let arguments = &Self::ARGUMENTS
|
||||
.iter()
|
||||
.map(|a| Arg::with_name(a.0).help(a.1).required(a.2).index(a.3))
|
||||
.map(|a| {
|
||||
let mut args = Arg::with_name(a.0).help(a.1).required(a.3).index(a.4);
|
||||
if a.2.len() > 0 {
|
||||
args = args.possible_values(a.2);
|
||||
}
|
||||
args
|
||||
})
|
||||
.collect::<Vec<Arg<'static, 'static>>>();
|
||||
let flags = &Self::FLAGS
|
||||
.iter()
|
||||
.map(|a| Arg::from_usage(a).global(true))
|
||||
.map(|a| Arg::from_usage(a))
|
||||
.collect::<Vec<Arg<'static, 'static>>>();
|
||||
let options = &Self::OPTIONS
|
||||
.iter()
|
||||
@ -56,6 +62,22 @@ pub trait CLI {
|
||||
.about(s.1)
|
||||
.args(
|
||||
&s.2.iter()
|
||||
.map(|a| {
|
||||
let mut args = Arg::with_name(a.0).help(a.1).required(a.3).index(a.4);
|
||||
if a.2.len() > 0 {
|
||||
args = args.possible_values(a.2);
|
||||
}
|
||||
args
|
||||
})
|
||||
.collect::<Vec<Arg<'static, 'static>>>(),
|
||||
)
|
||||
.args(
|
||||
&s.3.iter()
|
||||
.map(|a| Arg::from_usage(a))
|
||||
.collect::<Vec<Arg<'static, 'static>>>(),
|
||||
)
|
||||
.args(
|
||||
&s.4.iter()
|
||||
.map(|a| match a.2.len() > 0 {
|
||||
true => Arg::from_usage(a.0)
|
||||
.conflicts_with_all(a.1)
|
||||
@ -65,7 +87,7 @@ pub trait CLI {
|
||||
})
|
||||
.collect::<Vec<Arg<'static, 'static>>>(),
|
||||
)
|
||||
.settings(s.3)
|
||||
.settings(s.5)
|
||||
})
|
||||
.collect::<Vec<App<'static, 'static>>>();
|
||||
|
||||
|
@ -24,10 +24,14 @@ pub type DescriptionType = &'static str;
|
||||
|
||||
pub type RequiredType = bool;
|
||||
|
||||
pub type PossibleValuesType = &'static [&'static str];
|
||||
|
||||
pub type IndexType = u64;
|
||||
|
||||
pub type ArgumentType = (NameType, DescriptionType, RequiredType, IndexType);
|
||||
pub type ArgumentType = (NameType, DescriptionType, PossibleValuesType, RequiredType, IndexType);
|
||||
|
||||
// Format
|
||||
// "[flag] -f --flag 'Add flag description here'"
|
||||
pub type FlagType = &'static str;
|
||||
|
||||
// Format
|
||||
@ -39,4 +43,11 @@ pub type OptionType = (
|
||||
&'static [&'static str],
|
||||
);
|
||||
|
||||
pub type SubCommandType = (NameType, AboutType, &'static [OptionType], &'static [AppSettings]);
|
||||
pub type SubCommandType = (
|
||||
NameType,
|
||||
AboutType,
|
||||
&'static [ArgumentType],
|
||||
&'static [FlagType],
|
||||
&'static [OptionType],
|
||||
&'static [AppSettings],
|
||||
);
|
||||
|
@ -35,7 +35,7 @@ use std::{
|
||||
io::{Read, Write},
|
||||
};
|
||||
|
||||
pub const ADD_URL: &str = "api/package/fetch";
|
||||
pub const ADD_URL: &str = "v1/package/fetch";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AddCommand;
|
||||
@ -47,10 +47,11 @@ impl CLI for AddCommand {
|
||||
|
||||
const ABOUT: AboutType = "Install a package from the Aleo Package Manager";
|
||||
const ARGUMENTS: &'static [ArgumentType] = &[
|
||||
// (name, description, required, index)
|
||||
// (name, description, possible_values, required, index)
|
||||
(
|
||||
"REMOTE",
|
||||
"Install a package from the Aleo Package Manager with the given remote",
|
||||
&[],
|
||||
false,
|
||||
1u64,
|
||||
),
|
||||
|
@ -186,9 +186,9 @@ impl CLI for BuildCommand {
|
||||
// Drop "Compiling" context for console logging
|
||||
drop(enter);
|
||||
|
||||
// Begin "Finished" context for console logging todo: @collin figure a way to get this output with tracing without dropping span
|
||||
tracing::span!(tracing::Level::INFO, "Finished").in_scope(|| {
|
||||
tracing::info!("Completed in {} milliseconds\n", start.elapsed().as_millis());
|
||||
// Begin "Done" context for console logging todo: @collin figure a way to get this output with tracing without dropping span
|
||||
tracing::span!(tracing::Level::INFO, "Done").in_scope(|| {
|
||||
tracing::info!("Finished in {} milliseconds\n", start.elapsed().as_millis());
|
||||
});
|
||||
|
||||
return Ok(Some((program, checksum_differs)));
|
||||
|
@ -75,8 +75,8 @@ impl CLI for CleanCommand {
|
||||
// Drop "Compiling" context for console logging
|
||||
drop(enter);
|
||||
|
||||
// Begin "Finished" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Finished").in_scope(|| {
|
||||
// Begin "Done" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Done").in_scope(|| {
|
||||
tracing::info!("Program workspace cleaned\n");
|
||||
});
|
||||
|
||||
|
@ -30,7 +30,7 @@ use crate::{
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const LOGIN_URL: &str = "api/account/authenticate";
|
||||
pub const LOGIN_URL: &str = "v1/account/authenticate";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginCommand;
|
||||
@ -42,10 +42,11 @@ impl CLI for LoginCommand {
|
||||
|
||||
const ABOUT: AboutType = "Login to the Aleo Package Manager";
|
||||
const ARGUMENTS: &'static [ArgumentType] = &[
|
||||
// (name, description, required, index)
|
||||
// (name, description, possible_values, required, index)
|
||||
(
|
||||
"NAME",
|
||||
"Sets the authentication token for login to the package manager",
|
||||
&[],
|
||||
false,
|
||||
1u64,
|
||||
),
|
||||
|
@ -33,10 +33,11 @@ impl CLI for NewCommand {
|
||||
|
||||
const ABOUT: AboutType = "Create a new Leo package in a new directory";
|
||||
const ARGUMENTS: &'static [ArgumentType] = &[
|
||||
// (name, description, required, index)
|
||||
// (name, description, possible_values, required, index)
|
||||
(
|
||||
"NAME",
|
||||
"Sets the resulting package name, defaults to the directory name",
|
||||
&[],
|
||||
true,
|
||||
1u64,
|
||||
),
|
||||
|
@ -75,9 +75,9 @@ impl CLI for ProveCommand {
|
||||
// Drop "Prover" context for console logging
|
||||
drop(enter);
|
||||
|
||||
// Begin "Finished" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Finished").in_scope(|| {
|
||||
tracing::info!("Completed in {:?} milliseconds\n", end);
|
||||
// Begin "Done" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Done").in_scope(|| {
|
||||
tracing::info!("Finished in {:?} milliseconds\n", end);
|
||||
});
|
||||
|
||||
Ok((program_proof, prepared_verifying_key))
|
||||
|
@ -38,7 +38,7 @@ use reqwest::{
|
||||
use serde::Deserialize;
|
||||
use std::{convert::TryFrom, env::current_dir};
|
||||
|
||||
const PUBLISH_URL: &str = "api/package/publish";
|
||||
pub const PUBLISH_URL: &str = "v1/package/publish";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ResponseJson {
|
||||
|
@ -29,8 +29,14 @@ impl CLI for RemoveCommand {
|
||||
|
||||
const ABOUT: AboutType = "Uninstall a package from the current package";
|
||||
const ARGUMENTS: &'static [ArgumentType] = &[
|
||||
// (name, description, required, index)
|
||||
("NAME", "Removes the package from the current directory", true, 1u64),
|
||||
// (name, description, possible_values, required, index)
|
||||
(
|
||||
"NAME",
|
||||
"Removes the package from the current directory",
|
||||
&[],
|
||||
true,
|
||||
1u64,
|
||||
),
|
||||
];
|
||||
const FLAGS: &'static [FlagType] = &[];
|
||||
const NAME: NameType = "remove";
|
||||
|
@ -48,7 +48,7 @@ impl CLI for RunCommand {
|
||||
let (proof, prepared_verifying_key) = ProveCommand::output(options)?;
|
||||
|
||||
// Begin "Verifying" context for console logging
|
||||
let span = tracing::span!(tracing::Level::INFO, "Verifier");
|
||||
let span = tracing::span!(tracing::Level::INFO, "Verifying");
|
||||
let enter = span.enter();
|
||||
|
||||
tracing::info!("Starting...");
|
||||
@ -76,9 +76,9 @@ impl CLI for RunCommand {
|
||||
// Drop "Verifying" context for console logging
|
||||
drop(enter);
|
||||
|
||||
// Begin "Finished" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Finished").in_scope(|| {
|
||||
tracing::info!("Completed in {:?} milliseconds\n", end);
|
||||
// Begin "Done" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Done").in_scope(|| {
|
||||
tracing::info!("Finished in {:?} milliseconds\n", end);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
@ -138,9 +138,9 @@ impl CLI for SetupCommand {
|
||||
// Drop "Setup" context for console logging
|
||||
drop(enter);
|
||||
|
||||
// Begin "Finished" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Finished").in_scope(|| {
|
||||
tracing::info!("Completed in {:?} milliseconds\n", end);
|
||||
// Begin "Done" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Done").in_scope(|| {
|
||||
tracing::info!("Finished in {:?} milliseconds\n", end);
|
||||
});
|
||||
|
||||
Ok((program, proving_key, prepared_verifying_key))
|
||||
|
@ -110,8 +110,8 @@ impl CLI for TestCommand {
|
||||
|
||||
// Set the result of the test command to passed if no tests failed.
|
||||
if failed == 0 {
|
||||
// Begin "Finished" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Finished").in_scope(|| {
|
||||
// Begin "Done" context for console logging
|
||||
tracing::span!(tracing::Level::INFO, "Done").in_scope(|| {
|
||||
tracing::info!(
|
||||
"Tests passed in {} milliseconds. {} passed; {} failed;\n",
|
||||
start.elapsed().as_millis(),
|
||||
@ -120,8 +120,8 @@ impl CLI for TestCommand {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Begin "Finished" context for console logging
|
||||
tracing::span!(tracing::Level::ERROR, "Finished").in_scope(|| {
|
||||
// Begin "Done" context for console logging
|
||||
tracing::span!(tracing::Level::ERROR, "Done").in_scope(|| {
|
||||
tracing::error!(
|
||||
"Tests failed in {} milliseconds. {} passed; {} failed;\n",
|
||||
start.elapsed().as_millis(),
|
||||
|
@ -14,25 +14,59 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{cli::CLI, cli_types::*, updater::Updater};
|
||||
use crate::{cli::CLI, cli_types::*, config::Config, updater::Updater};
|
||||
|
||||
use clap::AppSettings;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateCommand;
|
||||
|
||||
impl CLI for UpdateCommand {
|
||||
type Options = (bool,);
|
||||
// (show_all_versions, quiet)
|
||||
type Options = Option<(bool, bool, bool)>;
|
||||
type Output = ();
|
||||
|
||||
const ABOUT: AboutType = "Update Leo to the latest version";
|
||||
const ARGUMENTS: &'static [ArgumentType] = &[];
|
||||
const FLAGS: &'static [FlagType] = &[("--list")];
|
||||
const FLAGS: &'static [FlagType] = &[
|
||||
"[list] -l --list 'List all available versions of Leo'",
|
||||
"[quiet] -q --quiet 'Suppress outputs to terminal'",
|
||||
"[studio] -s --studio 'For Aleo Studio only'",
|
||||
];
|
||||
const NAME: NameType = "update";
|
||||
const OPTIONS: &'static [OptionType] = &[];
|
||||
const SUBCOMMANDS: &'static [SubCommandType] = &[];
|
||||
const SUBCOMMANDS: &'static [SubCommandType] = &[
|
||||
// (name, description, options, settings)
|
||||
(
|
||||
UpdateAutomatic::NAME,
|
||||
UpdateAutomatic::ABOUT,
|
||||
UpdateAutomatic::ARGUMENTS,
|
||||
UpdateAutomatic::FLAGS,
|
||||
&UpdateAutomatic::OPTIONS,
|
||||
&[
|
||||
AppSettings::ColoredHelp,
|
||||
AppSettings::DisableHelpSubcommand,
|
||||
AppSettings::DisableVersion,
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
fn parse(arguments: &clap::ArgMatches) -> Result<Self::Options, crate::errors::CLIError> {
|
||||
match arguments.subcommand() {
|
||||
("automatic", Some(arguments)) => {
|
||||
// Run the `automatic` subcommand
|
||||
let options = UpdateAutomatic::parse(arguments)?;
|
||||
let _output = UpdateAutomatic::output(options)?;
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let show_all_versions = arguments.is_present("list");
|
||||
Ok((show_all_versions,))
|
||||
let quiet = arguments.is_present("quiet");
|
||||
let studio = arguments.is_present("studio");
|
||||
|
||||
Ok(Some((show_all_versions, quiet, studio)))
|
||||
}
|
||||
|
||||
fn output(options: Self::Options) -> Result<Self::Output, crate::errors::CLIError> {
|
||||
@ -40,29 +74,123 @@ impl CLI for UpdateCommand {
|
||||
let span = tracing::span!(tracing::Level::INFO, "Updating");
|
||||
let _enter = span.enter();
|
||||
|
||||
match options {
|
||||
(true,) => match Updater::show_available_releases() {
|
||||
let (show_all_versions, quiet, studio) = match options {
|
||||
Some(options) => options,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
match show_all_versions {
|
||||
true => match Updater::show_available_releases() {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
tracing::error!("Could not fetch that latest version of Leo");
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
},
|
||||
(false,) => match Updater::update_to_latest_release(true) {
|
||||
Ok(status) => {
|
||||
if status.uptodate() {
|
||||
tracing::info!("Leo is already on the latest version: {}", status.version());
|
||||
} else if status.updated() {
|
||||
tracing::info!("Leo has successfully updated to version: {}", status.version());
|
||||
}
|
||||
false => {
|
||||
let config = Config::read_config()?;
|
||||
|
||||
// If update is run with studio and the automatic update is off, finish quietly
|
||||
if studio && !config.update.automatic {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Could not update Leo to the latest version");
|
||||
tracing::error!("{}", e);
|
||||
|
||||
match Updater::update_to_latest_release(!quiet) {
|
||||
Ok(status) => {
|
||||
if !quiet {
|
||||
if status.uptodate() {
|
||||
tracing::info!("Leo is already on the latest version {}", status.version());
|
||||
} else if status.updated() {
|
||||
tracing::info!("Leo has successfully updated to version {}", status.version());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
if !quiet {
|
||||
tracing::error!("Could not update Leo to the latest version");
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//TODO (raychu86) Move this to dedicated file/module
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateAutomatic;
|
||||
|
||||
impl CLI for UpdateAutomatic {
|
||||
// (is_automatic, quiet)
|
||||
type Options = (Option<bool>, bool);
|
||||
type Output = ();
|
||||
|
||||
const ABOUT: AboutType = "Setting for automatic updates of Leo";
|
||||
const ARGUMENTS: &'static [ArgumentType] = &[
|
||||
// (name, description, possible_values, required, index)
|
||||
(
|
||||
"automatic",
|
||||
"Enable or disable automatic updates",
|
||||
&["true", "false"],
|
||||
false,
|
||||
1u64,
|
||||
),
|
||||
];
|
||||
const FLAGS: &'static [FlagType] = &["[quiet] -q --quiet 'Suppress outputs to terminal'"];
|
||||
const NAME: NameType = "automatic";
|
||||
const OPTIONS: &'static [OptionType] = &[];
|
||||
const SUBCOMMANDS: &'static [SubCommandType] = &[];
|
||||
|
||||
fn parse(arguments: &clap::ArgMatches) -> Result<Self::Options, crate::errors::CLIError> {
|
||||
let quiet = arguments.is_present("quiet");
|
||||
|
||||
match arguments.value_of("automatic") {
|
||||
Some(automatic) => {
|
||||
// TODO enforce that the possible values is true or false
|
||||
let automatic = match automatic {
|
||||
"true" => Some(true),
|
||||
"false" => Some(false),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok((automatic, quiet))
|
||||
}
|
||||
None => Ok((None, quiet)),
|
||||
}
|
||||
}
|
||||
|
||||
fn output(options: Self::Options) -> Result<Self::Output, crate::errors::CLIError> {
|
||||
// Begin "Settings" context for console logging
|
||||
let span = tracing::span!(tracing::Level::INFO, "Settings");
|
||||
let enter = span.enter();
|
||||
|
||||
// If a boolean value is provided, update the saved
|
||||
// `automatic` configuration value to this boolean value.
|
||||
if let Some(automatic) = options.0 {
|
||||
Config::set_update_automatic(automatic)?;
|
||||
}
|
||||
|
||||
// If --quiet is not enabled, log the output.
|
||||
if !options.1 {
|
||||
// Read the `automatic` value now.
|
||||
let automatic = Config::read_config()?.update.automatic;
|
||||
|
||||
// Log the output.
|
||||
tracing::debug!("automatic = {}", automatic);
|
||||
match automatic {
|
||||
true => tracing::info!("Automatic updates are enabled. Leo will update as new versions are released."),
|
||||
false => {
|
||||
tracing::info!("Automatic updates are disabled. Leo will not update as new versions are released.")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Drop "Settings" context for console logging.
|
||||
drop(enter);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub const PACKAGE_MANAGER_URL: &str = "https://apm-backend-prod.herokuapp.com/";
|
||||
pub const PACKAGE_MANAGER_URL: &str = "https://api.aleo.pm/";
|
||||
|
||||
pub const LEO_CREDENTIALS_FILE: &str = "credentials";
|
||||
pub const LEO_CONFIG_FILE: &str = "config.toml";
|
||||
@ -49,14 +49,27 @@ lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Update {
|
||||
pub automatic: bool,
|
||||
}
|
||||
|
||||
impl Default for Update {
|
||||
fn default() -> Self {
|
||||
Self { automatic: true }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub auto_update: bool,
|
||||
pub update: Update,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self { auto_update: false }
|
||||
Self {
|
||||
update: Update::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,10 +89,19 @@ impl Config {
|
||||
}
|
||||
|
||||
let toml_string = match fs::read_to_string(&config_path) {
|
||||
Ok(toml) => toml,
|
||||
Ok(mut toml) => {
|
||||
// If the config is using an incorrect format, rewrite it.
|
||||
if let Err(_) = toml::from_str::<Config>(&toml) {
|
||||
let default_config_string = toml::to_string(&Config::default())?;
|
||||
fs::write(&config_path, default_config_string.clone())?;
|
||||
toml = default_config_string;
|
||||
}
|
||||
|
||||
toml
|
||||
}
|
||||
Err(_) => {
|
||||
create_dir_all(&config_dir)?;
|
||||
String::new()
|
||||
toml::to_string(&Config::default())?
|
||||
}
|
||||
};
|
||||
|
||||
@ -88,6 +110,21 @@ impl Config {
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Update the `automatic` configuration in the `config.toml` file.
|
||||
pub fn set_update_automatic(automatic: bool) -> Result<(), CLIError> {
|
||||
let mut config = Self::read_config()?;
|
||||
|
||||
if config.update.automatic != automatic {
|
||||
config.update.automatic = automatic;
|
||||
|
||||
// Update the config file
|
||||
let config_path = LEO_CONFIG_PATH.clone();
|
||||
fs::write(&config_path, toml::to_string(&config)?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_token(token: &str) -> Result<(), io::Error> {
|
||||
|
@ -186,7 +186,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
write!(writer, "{:>12} ", colored_string(meta.level(), &message)).expect("Error writing event");
|
||||
write!(writer, "{:>10} ", colored_string(meta.level(), &message)).expect("Error writing event");
|
||||
}
|
||||
|
||||
ctx.format_fields(writer, event)?;
|
||||
|
@ -33,7 +33,7 @@ impl Updater {
|
||||
.repo_name(Self::LEO_REPO_NAME)
|
||||
.bin_name(Self::LEO_BIN_NAME)
|
||||
.current_version(&include_str!("./leo-version").replace('v', ""))
|
||||
.show_download_progress(true)
|
||||
.show_download_progress(show_output)
|
||||
.no_confirm(true)
|
||||
.show_output(show_output)
|
||||
.build()?
|
||||
@ -65,7 +65,7 @@ impl Updater {
|
||||
pub fn print_cli() {
|
||||
let config = Config::read_config().unwrap();
|
||||
|
||||
if config.auto_update {
|
||||
if config.update.automatic {
|
||||
// If the auto update configuration is on, attempt to update the version.
|
||||
if let Ok(status) = Self::update_to_latest_release(false) {
|
||||
if status.updated() {
|
||||
|
Loading…
Reference in New Issue
Block a user