2018-12-11 16:43:26 +02:00
|
|
|
use super::error::{Error, ErrorKind};
|
2019-08-22 22:29:31 +03:00
|
|
|
use super::utils::editor;
|
2018-07-07 02:18:19 +03:00
|
|
|
use directories::BaseDirs;
|
2018-12-11 16:43:26 +02:00
|
|
|
use failure::ResultExt;
|
2019-10-10 11:26:00 +03:00
|
|
|
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
|
2019-09-28 14:41:06 +03:00
|
|
|
|
2019-08-22 22:08:28 +03:00
|
|
|
use log::{debug, error, LevelFilter};
|
|
|
|
|
use pretty_env_logger::formatted_timed_builder;
|
2018-12-24 10:09:46 +02:00
|
|
|
use serde::Deserialize;
|
2018-06-11 13:56:57 +03:00
|
|
|
use shellexpand;
|
2019-09-28 14:41:06 +03:00
|
|
|
use std::collections::BTreeMap;
|
2019-08-22 22:08:28 +03:00
|
|
|
use std::fs::write;
|
2019-08-22 22:29:31 +03:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::process::Command;
|
2019-06-16 09:09:05 +03:00
|
|
|
use std::{env, fs};
|
2018-11-18 14:25:16 +02:00
|
|
|
use structopt::StructOpt;
|
2018-06-08 18:19:07 +03:00
|
|
|
use toml;
|
|
|
|
|
|
2018-06-20 20:26:08 +03:00
|
|
|
type Commands = BTreeMap<String, String>;
|
|
|
|
|
|
2019-10-10 11:26:00 +03:00
|
|
|
#[derive(EnumString, EnumVariantNames, Debug, Clone, PartialEq, Deserialize, EnumIter)]
|
2019-01-22 22:37:32 +02:00
|
|
|
#[serde(rename_all = "lowercase")]
|
2019-09-28 14:41:06 +03:00
|
|
|
#[strum(serialize_all = "snake_case")]
|
2019-01-16 08:51:43 +00:00
|
|
|
pub enum Step {
|
|
|
|
|
/// Don't perform system upgrade
|
|
|
|
|
System,
|
2019-09-28 14:49:24 +03:00
|
|
|
/// Don't perform upgrades of package managers
|
|
|
|
|
PackageManagers,
|
2019-07-01 09:20:48 +03:00
|
|
|
/// Don't pull git repositories
|
2019-01-16 08:51:43 +00:00
|
|
|
GitRepos,
|
|
|
|
|
/// Don't upgrade Vim packages or configuration files
|
|
|
|
|
Vim,
|
|
|
|
|
/// Don't upgrade Emacs packages or configuration files
|
|
|
|
|
Emacs,
|
|
|
|
|
/// Don't upgrade ruby gems
|
|
|
|
|
Gem,
|
2019-09-04 12:57:22 +02:00
|
|
|
/// Don't upgrade npm/composer/yarn packages
|
|
|
|
|
Node,
|
2019-06-13 12:19:47 +02:00
|
|
|
/// Don't upgrade SDKMAN! and its packages
|
|
|
|
|
Sdkman,
|
2019-07-01 08:53:53 +03:00
|
|
|
/// Don't run remote Togprades
|
|
|
|
|
Remotes,
|
2019-08-19 20:15:01 +03:00
|
|
|
/// Don't run Rustup
|
|
|
|
|
Rustup,
|
|
|
|
|
/// Don't run Cargo
|
|
|
|
|
Cargo,
|
2019-01-27 20:15:59 +02:00
|
|
|
/// Don't update Powershell modules
|
2019-09-05 13:38:45 -04:00
|
|
|
Shell,
|
2019-01-16 08:51:43 +00:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 22:08:28 +03:00
|
|
|
#[derive(Deserialize, Default, Debug)]
|
2019-08-22 21:45:42 +03:00
|
|
|
#[serde(deny_unknown_fields)]
|
2019-01-22 22:37:32 +02:00
|
|
|
/// Configuration file
|
|
|
|
|
pub struct ConfigFile {
|
2018-06-20 20:26:08 +03:00
|
|
|
pre_commands: Option<Commands>,
|
|
|
|
|
commands: Option<Commands>,
|
2018-06-08 18:19:07 +03:00
|
|
|
git_repos: Option<Vec<String>>,
|
2019-01-22 22:37:32 +02:00
|
|
|
disable: Option<Vec<Step>>,
|
2019-06-05 14:15:45 +03:00
|
|
|
remote_topgrades: Option<Vec<String>>,
|
2019-09-01 20:45:44 +02:00
|
|
|
ssh_arguments: Option<String>,
|
2019-09-04 21:31:23 +03:00
|
|
|
git_arguments: Option<String>,
|
2019-09-05 20:52:51 +03:00
|
|
|
set_title: Option<bool>,
|
2019-09-28 15:45:05 +03:00
|
|
|
assume_yes: Option<bool>,
|
2019-10-03 08:12:43 +03:00
|
|
|
yay_arguments: Option<String>,
|
2019-10-10 11:26:00 +03:00
|
|
|
no_retry: Option<bool>,
|
|
|
|
|
run_in_tmux: Option<bool>,
|
|
|
|
|
verbose: Option<bool>,
|
|
|
|
|
cleanup: Option<bool>,
|
|
|
|
|
only: Option<Vec<Step>>,
|
2018-06-08 18:19:07 +03:00
|
|
|
}
|
|
|
|
|
|
2019-01-22 22:37:32 +02:00
|
|
|
impl ConfigFile {
|
2019-08-22 22:29:31 +03:00
|
|
|
fn ensure(base_dirs: &BaseDirs) -> Result<PathBuf, Error> {
|
2018-06-08 18:19:07 +03:00
|
|
|
let config_path = base_dirs.config_dir().join("topgrade.toml");
|
|
|
|
|
if !config_path.exists() {
|
2019-08-22 22:08:28 +03:00
|
|
|
write(&config_path, include_str!("../config.example.toml"))
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
error!(
|
|
|
|
|
"Unable to write the example configuration file to {}: {}",
|
|
|
|
|
config_path.display(),
|
|
|
|
|
e
|
|
|
|
|
);
|
2019-08-22 22:29:31 +03:00
|
|
|
e
|
2019-08-22 22:08:28 +03:00
|
|
|
})
|
2019-08-22 22:29:31 +03:00
|
|
|
.context(ErrorKind::Configuration)?;
|
2019-08-22 22:08:28 +03:00
|
|
|
debug!("No configuration exists");
|
2018-06-08 18:19:07 +03:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 22:29:31 +03:00
|
|
|
Ok(config_path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read the configuration file.
|
|
|
|
|
///
|
|
|
|
|
/// If the configuration file does not exist the function returns the default ConfigFile.
|
|
|
|
|
fn read(base_dirs: &BaseDirs) -> Result<ConfigFile, Error> {
|
|
|
|
|
let config_path = Self::ensure(base_dirs)?;
|
2018-12-11 16:43:26 +02:00
|
|
|
let mut result: Self = toml::from_str(&fs::read_to_string(config_path).context(ErrorKind::Configuration)?)
|
|
|
|
|
.context(ErrorKind::Configuration)?;
|
2018-06-11 13:56:57 +03:00
|
|
|
|
|
|
|
|
if let Some(ref mut paths) = &mut result.git_repos {
|
|
|
|
|
for path in paths.iter_mut() {
|
|
|
|
|
*path = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-22 22:08:28 +03:00
|
|
|
debug!("Loaded configuration: {:?}", result);
|
|
|
|
|
|
2018-06-11 13:56:57 +03:00
|
|
|
Ok(result)
|
2018-06-08 18:19:07 +03:00
|
|
|
}
|
2019-08-22 22:29:31 +03:00
|
|
|
|
|
|
|
|
fn edit(base_dirs: &BaseDirs) -> Result<(), Error> {
|
|
|
|
|
let config_path = Self::ensure(base_dirs)?;
|
|
|
|
|
let editor = editor();
|
|
|
|
|
|
|
|
|
|
debug!("Editing {} with {}", config_path.display(), editor);
|
|
|
|
|
Command::new(editor)
|
|
|
|
|
.arg(config_path)
|
|
|
|
|
.spawn()
|
|
|
|
|
.and_then(|mut p| p.wait())
|
|
|
|
|
.context(ErrorKind::Configuration)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2018-06-08 18:19:07 +03:00
|
|
|
}
|
2018-09-06 14:42:56 +03:00
|
|
|
|
|
|
|
|
#[derive(StructOpt, Debug)]
|
2019-09-04 21:07:43 +03:00
|
|
|
#[structopt(name = "Topgrade", setting = structopt::clap::AppSettings::ColoredHelp)]
|
2019-01-22 22:37:32 +02:00
|
|
|
/// Command line arguments
|
|
|
|
|
pub struct CommandLineArgs {
|
2019-08-22 22:29:31 +03:00
|
|
|
/// Edit the configuration file
|
|
|
|
|
#[structopt(long = "edit-config")]
|
|
|
|
|
edit_config: bool,
|
|
|
|
|
|
2018-12-05 13:29:31 +02:00
|
|
|
/// Run inside tmux
|
|
|
|
|
#[structopt(short = "t", long = "tmux")]
|
2019-01-22 22:37:32 +02:00
|
|
|
run_in_tmux: bool,
|
2018-09-06 14:42:56 +03:00
|
|
|
|
2018-12-05 13:29:31 +02:00
|
|
|
/// Cleanup temporary or old files
|
|
|
|
|
#[structopt(short = "c", long = "cleanup")]
|
2019-01-22 22:37:32 +02:00
|
|
|
cleanup: bool,
|
2018-11-17 19:09:46 +01:00
|
|
|
|
2018-12-05 13:29:31 +02:00
|
|
|
/// Print what would be done
|
|
|
|
|
#[structopt(short = "n", long = "dry-run")]
|
2019-01-22 22:37:32 +02:00
|
|
|
dry_run: bool,
|
2018-12-05 13:38:18 +02:00
|
|
|
|
|
|
|
|
/// Do not ask to retry failed steps
|
|
|
|
|
#[structopt(long = "no-retry")]
|
2019-01-22 22:37:32 +02:00
|
|
|
no_retry: bool,
|
2019-01-16 08:51:43 +00:00
|
|
|
|
|
|
|
|
/// Do not perform upgrades for the given steps
|
2019-09-28 14:41:06 +03:00
|
|
|
#[structopt(long = "disable", possible_values = &Step::variants())]
|
2019-01-22 22:37:32 +02:00
|
|
|
disable: Vec<Step>,
|
2019-06-05 11:44:28 +03:00
|
|
|
|
2019-09-28 15:17:06 +03:00
|
|
|
/// Perform only the specified steps (experimental)
|
|
|
|
|
#[structopt(long = "only", possible_values = &Step::variants())]
|
|
|
|
|
only: Vec<Step>,
|
|
|
|
|
|
2019-06-05 11:44:28 +03:00
|
|
|
/// Output logs
|
|
|
|
|
#[structopt(short = "v", long = "verbose")]
|
|
|
|
|
verbose: bool,
|
2019-06-16 09:09:05 +03:00
|
|
|
|
2019-09-01 13:44:20 -05:00
|
|
|
/// Prompt for a key before exiting
|
2019-06-16 09:09:05 +03:00
|
|
|
#[structopt(short = "k", long = "keep")]
|
|
|
|
|
keep_at_end: bool,
|
2019-09-28 15:45:05 +03:00
|
|
|
|
|
|
|
|
/// Say yes to package manager's prompt (experimental)
|
|
|
|
|
#[structopt(short = "y", long = "yes")]
|
|
|
|
|
yes: bool,
|
2019-01-22 22:37:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Represents the application configuration
|
|
|
|
|
///
|
|
|
|
|
/// The struct holds the loaded configuration file, as well as the arguments parsed from the command line.
|
|
|
|
|
/// Its provided methods decide the appropriate options based on combining the configuraiton file and the
|
|
|
|
|
/// command line arguments.
|
|
|
|
|
pub struct Config {
|
|
|
|
|
opt: CommandLineArgs,
|
|
|
|
|
config_file: ConfigFile,
|
2019-10-10 11:26:00 +03:00
|
|
|
allowed_steps: Vec<Step>,
|
2019-01-22 22:37:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Config {
|
|
|
|
|
/// Load the configuration.
|
|
|
|
|
///
|
|
|
|
|
/// The function parses the command line arguments and reading the configuration file.
|
|
|
|
|
pub fn load(base_dirs: &BaseDirs) -> Result<Self, Error> {
|
2019-08-22 22:08:28 +03:00
|
|
|
let opt = CommandLineArgs::from_args();
|
2019-10-10 11:26:00 +03:00
|
|
|
let config_file = ConfigFile::read(base_dirs)?;
|
2019-08-22 22:08:28 +03:00
|
|
|
|
|
|
|
|
let mut builder = formatted_timed_builder();
|
|
|
|
|
|
2019-10-10 11:26:00 +03:00
|
|
|
if opt.verbose || config_file.verbose.unwrap_or(false) {
|
2019-08-22 22:08:28 +03:00
|
|
|
builder.filter(Some("topgrade"), LevelFilter::Trace);
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-10 11:26:00 +03:00
|
|
|
let allowed_steps = Self::allowed_steps(&opt, &config_file);
|
|
|
|
|
|
2019-08-22 22:08:28 +03:00
|
|
|
builder.init();
|
|
|
|
|
|
2019-01-22 22:37:32 +02:00
|
|
|
Ok(Self {
|
2019-08-22 22:08:28 +03:00
|
|
|
opt,
|
2019-10-10 11:26:00 +03:00
|
|
|
config_file,
|
|
|
|
|
allowed_steps,
|
2019-01-22 22:37:32 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-22 22:29:31 +03:00
|
|
|
/// Launch an editor to edit the configuration
|
|
|
|
|
pub fn edit(base_dirs: &BaseDirs) -> Result<(), Error> {
|
|
|
|
|
ConfigFile::edit(base_dirs)
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-22 22:37:32 +02:00
|
|
|
/// The list of commands to run before performing any step.
|
|
|
|
|
pub fn pre_commands(&self) -> &Option<Commands> {
|
|
|
|
|
&self.config_file.pre_commands
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The list of custom steps.
|
|
|
|
|
pub fn commands(&self) -> &Option<Commands> {
|
|
|
|
|
&self.config_file.commands
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The list of additional git repositories to pull.
|
|
|
|
|
pub fn git_repos(&self) -> &Option<Vec<String>> {
|
|
|
|
|
&self.config_file.git_repos
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tell whether the specified step should run.
|
|
|
|
|
///
|
|
|
|
|
/// If the step appears either in the `--disable` command line argument
|
|
|
|
|
/// or the `disable` option in the configuration, the function returns false.
|
|
|
|
|
pub fn should_run(&self, step: Step) -> bool {
|
2019-10-10 11:26:00 +03:00
|
|
|
self.allowed_steps.contains(&step)
|
|
|
|
|
}
|
2019-09-28 15:17:06 +03:00
|
|
|
|
2019-10-10 11:26:00 +03:00
|
|
|
fn allowed_steps(opt: &CommandLineArgs, config_file: &ConfigFile) -> Vec<Step> {
|
|
|
|
|
let mut enabled_steps: Vec<Step> = if !opt.only.is_empty() {
|
|
|
|
|
opt.only.clone()
|
|
|
|
|
} else {
|
|
|
|
|
config_file
|
|
|
|
|
.only
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map_or_else(|| Step::iter().collect(), |v| v.clone())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let disabled_steps: Vec<Step> = if !opt.disable.is_empty() {
|
|
|
|
|
opt.disable.clone()
|
|
|
|
|
} else {
|
|
|
|
|
config_file.disable.as_ref().map_or_else(|| vec![], |v| v.clone())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
enabled_steps.retain(|e| !disabled_steps.contains(e));
|
|
|
|
|
enabled_steps
|
2019-01-22 22:37:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tell whether we should run in tmux.
|
|
|
|
|
pub fn run_in_tmux(&self) -> bool {
|
2019-10-10 11:26:00 +03:00
|
|
|
self.opt.run_in_tmux || self.config_file.run_in_tmux.unwrap_or(false)
|
2019-01-22 22:37:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tell whether we should perform cleanup steps.
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
|
pub fn cleanup(&self) -> bool {
|
2019-10-10 11:26:00 +03:00
|
|
|
self.opt.cleanup || self.config_file.cleanup.unwrap_or(false)
|
2019-01-22 22:37:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tell whether we are dry-running.
|
|
|
|
|
pub fn dry_run(&self) -> bool {
|
|
|
|
|
self.opt.dry_run
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tell whether we should not attempt to retry anything.
|
|
|
|
|
pub fn no_retry(&self) -> bool {
|
2019-10-10 11:26:00 +03:00
|
|
|
self.opt.no_retry || self.config_file.no_retry.unwrap_or(false)
|
2019-01-22 22:37:32 +02:00
|
|
|
}
|
2019-06-05 11:44:28 +03:00
|
|
|
|
2019-06-05 14:15:45 +03:00
|
|
|
/// List of remote hosts to run Topgrade in
|
|
|
|
|
pub fn remote_topgrades(&self) -> &Option<Vec<String>> {
|
|
|
|
|
&self.config_file.remote_topgrades
|
|
|
|
|
}
|
2019-06-16 09:09:05 +03:00
|
|
|
|
2019-09-01 20:45:44 +02:00
|
|
|
/// Extra SSH arguments
|
|
|
|
|
pub fn ssh_arguments(&self) -> &Option<String> {
|
|
|
|
|
&self.config_file.ssh_arguments
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-04 21:31:23 +03:00
|
|
|
/// Extra Git arguments
|
|
|
|
|
pub fn git_arguments(&self) -> &Option<String> {
|
|
|
|
|
&self.config_file.git_arguments
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-16 09:09:05 +03:00
|
|
|
/// Prompt for a key before exiting
|
|
|
|
|
pub fn keep_at_end(&self) -> bool {
|
|
|
|
|
self.opt.keep_at_end || env::var("TOPGRADE_KEEP_END").is_ok()
|
|
|
|
|
}
|
2019-08-22 22:29:31 +03:00
|
|
|
|
|
|
|
|
/// Whether to edit the configuration file
|
|
|
|
|
pub fn edit_config(&self) -> bool {
|
|
|
|
|
self.opt.edit_config
|
|
|
|
|
}
|
2019-09-05 20:52:51 +03:00
|
|
|
|
|
|
|
|
/// Whether to set the terminal title
|
|
|
|
|
pub fn set_title(&self) -> bool {
|
|
|
|
|
self.config_file.set_title.unwrap_or(true)
|
|
|
|
|
}
|
2019-09-28 15:45:05 +03:00
|
|
|
|
|
|
|
|
/// Whether to say yes to package managers
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn yes(&self) -> bool {
|
|
|
|
|
self.config_file.assume_yes.unwrap_or(self.opt.yes)
|
|
|
|
|
}
|
2019-10-03 08:12:43 +03:00
|
|
|
|
|
|
|
|
/// Extra yay arguments
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
pub fn yay_arguments(&self) -> &str {
|
|
|
|
|
match &self.config_file.yay_arguments {
|
|
|
|
|
Some(args) => args.as_str(),
|
|
|
|
|
None => "--devel",
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-06 14:42:56 +03:00
|
|
|
}
|