Added ability to include directories as an extension of the config file (#421)

This commit is contained in:
PolpOnline
2023-05-25 12:22:11 +02:00
committed by GitHub
parent 7c3ba80270
commit cb7adc8ced
5 changed files with 573 additions and 130 deletions

37
Cargo.lock generated
View File

@@ -1088,6 +1088,28 @@ dependencies = [
"autocfg",
]
[[package]]
name = "merge"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9"
dependencies = [
"merge_derive",
"num-traits",
]
[[package]]
name = "merge_derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -1494,9 +1516,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.5.6"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
dependencies = [
"aho-corasick",
"memchr",
@@ -1512,6 +1534,15 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-split"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "544861d1810a3e429bb5e80266e537138b0e69e59bed9334326ae129a4c3e676"
dependencies = [
"regex",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
@@ -2137,11 +2168,13 @@ dependencies = [
"home",
"lazy_static",
"libc",
"merge",
"nix 0.24.3",
"notify-rust",
"once_cell",
"parselnk",
"regex",
"regex-split",
"rust-ini",
"self_update",
"semver",

View File

@@ -41,12 +41,14 @@ tempfile = "~3.2"
cfg-if = "~1.0"
tokio = { version = "~1.18", features = ["process", "rt-multi-thread"] }
futures = "~0.3"
regex = "~1.5"
regex = "~1.7"
semver = "~1.0"
shell-words = "~1.1"
color-eyre = "~0.6"
tracing = { version = "~0.1", features = ["attributes", "log"] }
tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] }
merge = "0.1.0"
regex-split = "0.1.0"
[target.'cfg(target_os = "macos")'.dependencies]
notify-rust = "~4.5"

View File

@@ -1,3 +1,10 @@
# Include any additional configuration file(s)
# [include] sections are processed in the order you write them
# Files in $CONFIG_DIR/topgrade/topgrade.d/ are automatically included at the beginning of this file
[include]
#paths = ["/etc/topgrade.toml"]
[misc]
# Don't ask for confirmations
#assume_yes = true

View File

@@ -1,17 +1,20 @@
#![allow(dead_code)]
use std::collections::BTreeMap;
use std::fs::write;
use std::path::PathBuf;
use std::fs::{write, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use clap::{ArgEnum, Parser};
use clap_complete::Shell;
use color_eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use merge::Merge;
use regex::Regex;
use regex_split::RegexSplit;
use serde::Deserialize;
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
use tracing::debug;
@@ -19,6 +22,7 @@ use which_crate::which;
use crate::command::CommandExt;
use crate::sudo::SudoKind;
use crate::utils::string_prepend_str;
use super::utils::{editor, hostname};
@@ -51,21 +55,43 @@ macro_rules! check_deprecated {
}
};
}
macro_rules! get_deprecated {
($config:expr, $old:ident, $section:ident, $new:ident) => {
if $config.$old.is_some() {
&$config.$old
} else {
if let Some(section) = &$config.$section {
&section.$new
} else {
&None
/// Get a deprecated option moved from a section to another
macro_rules! get_deprecated_moved_opt {
($old_section:expr, $old:ident, $new_section:expr, $new:ident) => {{
if let Some(old_section) = &$old_section {
if old_section.$old.is_some() {
return &old_section.$old;
}
}
};
if let Some(new_section) = &$new_section {
return &new_section.$new;
}
return &None;
}};
}
type Commands = BTreeMap<String, String>;
macro_rules! get_deprecated_moved_or_default_to {
($old_section:expr, $old:ident, $new_section:expr, $new:ident, $default_ret:ident) => {{
if let Some(old_section) = &$old_section {
if let Some(old) = old_section.$old {
return old;
}
}
if let Some(new_section) = &$new_section {
if let Some(new) = new_section.$new {
return new;
}
}
return $default_ret;
}};
}
pub type Commands = BTreeMap<String, String>;
#[derive(ArgEnum, EnumString, EnumVariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy)]
#[clap(rename_all = "snake_case")]
@@ -169,24 +195,38 @@ pub enum Step {
Yarn,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Include {
#[merge(strategy = merge::vec::append)]
paths: Vec<String>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Git {
max_concurrency: Option<usize>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
repos: Option<Vec<String>>,
pull_predefined: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Vagrant {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
directories: Option<Vec<String>>,
power_on: Option<bool>,
always_suspend: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Windows {
accept_all_updates: Option<bool>,
@@ -197,7 +237,7 @@ pub struct Windows {
wsl_update_use_web_download: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Python {
enable_pip_review: Option<bool>,
@@ -205,43 +245,45 @@ pub struct Python {
enable_pipupgrade: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Distrobox {
use_root: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
containers: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Yarn {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct NPM {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Firmware {
upgrade: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Flatpak {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Brew {
greedy_cask: Option<bool>,
@@ -262,88 +304,191 @@ pub enum ArchPackageManager {
Yay,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Linux {
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
yay_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
aura_aur_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
aura_pacman_arguments: Option<String>,
arch_package_manager: Option<ArchPackageManager>,
show_arch_news: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
garuda_update_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
trizen_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
pikaur_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
pamac_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
dnf_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
nix_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
apt_arguments: Option<String>,
enable_tlmgr: Option<bool>,
redhat_distro_sync: Option<bool>,
suse_dup: Option<bool>,
rpm_ostree: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
emerge_sync_flags: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
emerge_update_flags: Option<String>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Composer {
self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Vim {
force_plug_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Misc {
pre_sudo: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
git_repos: Option<Vec<String>>,
predefined_git_repos: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
disable: Option<Vec<Step>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
ignore_failures: Option<Vec<Step>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
remote_topgrades: Option<Vec<String>>,
remote_topgrade_path: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
ssh_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
git_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
tmux_arguments: Option<String>,
set_title: Option<bool>,
display_time: Option<bool>,
display_preamble: Option<bool>,
assume_yes: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
yay_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
aura_aur_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
aura_pacman_arguments: Option<String>,
no_retry: Option<bool>,
run_in_tmux: Option<bool>,
cleanup: Option<bool>,
notify_each_step: Option<bool>,
accept_all_windows_updates: Option<bool>,
skip_notify: Option<bool>,
bashit_branch: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
only: Option<Vec<Step>>,
no_self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
/// Configuration file
pub struct ConfigFile {
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
include: Option<Include>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
misc: Option<Misc>,
sudo_command: Option<SudoKind>,
pre_sudo: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)]
pre_commands: Option<Commands>,
#[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)]
post_commands: Option<Commands>,
#[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)]
commands: Option<Commands>,
git_repos: Option<Vec<String>>,
predefined_git_repos: Option<bool>,
disable: Option<Vec<Step>>,
ignore_failures: Option<Vec<Step>>,
remote_topgrades: Option<Vec<String>>,
remote_topgrade_path: Option<String>,
ssh_arguments: Option<String>,
git_arguments: Option<String>,
tmux_arguments: Option<String>,
set_title: Option<bool>,
display_time: Option<bool>,
display_preamble: Option<bool>,
assume_yes: Option<bool>,
yay_arguments: Option<String>,
aura_aur_arguments: Option<String>,
aura_pacman_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
python: Option<Python>,
no_retry: Option<bool>,
run_in_tmux: Option<bool>,
cleanup: Option<bool>,
notify_each_step: Option<bool>,
accept_all_windows_updates: Option<bool>,
skip_notify: Option<bool>,
bashit_branch: Option<String>,
only: Option<Vec<Step>>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
composer: Option<Composer>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
brew: Option<Brew>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
linux: Option<Linux>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
git: Option<Git>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
windows: Option<Windows>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
npm: Option<NPM>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
yarn: Option<Yarn>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vim: Option<Vim>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
firmware: Option<Firmware>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vagrant: Option<Vagrant>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
flatpak: Option<Flatpak>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
distrobox: Option<Distrobox>,
no_self_update: Option<bool>,
}
fn config_directory() -> PathBuf {
@@ -354,60 +499,166 @@ fn config_directory() -> PathBuf {
return crate::WINDOWS_DIRS.config_dir();
}
/// The only purpose of this struct is to deserialize only the `include` field of the config file.
#[derive(Deserialize, Default, Debug)]
struct ConfigFileIncludeOnly {
include: Option<Include>,
}
impl ConfigFile {
fn ensure() -> Result<PathBuf> {
/// Returns the main config file and any additional config files
/// 0 = main config file
/// 1 = additional config files coming from topgrade.d
fn ensure() -> Result<(PathBuf, Vec<PathBuf>)> {
let mut res = (PathBuf::new(), Vec::new());
let config_directory = config_directory();
let config_path = config_directory.join("topgrade.toml");
let alt_config_path = config_directory.join("topgrade/topgrade.toml");
let possible_config_paths = vec![
config_directory.join("topgrade.toml"),
config_directory.join("topgrade/topgrade.toml"),
];
if config_path.exists() {
debug!("Configuration at {}", config_path.display());
Ok(config_path)
} else if alt_config_path.exists() {
debug!("Configuration at {}", alt_config_path.display());
Ok(alt_config_path)
} else {
// Search for the main config file
for path in possible_config_paths.iter() {
if path.exists() {
debug!("Configuration at {}", path.display());
res.0 = path.clone();
break;
}
}
res.1 = Self::ensure_topgrade_d(&config_directory)?;
// If no config file exists, create a default one in the config directory
if !res.0.exists() && res.1.is_empty() {
debug!("No configuration exists");
write(&config_path, EXAMPLE_CONFIG).map_err(|e| {
write(&res.0, EXAMPLE_CONFIG).map_err(|e| {
debug!(
"Unable to write the example configuration file to {}: {}. Using blank config.",
config_path.display(),
&res.0.display(),
e
);
e
})?;
Ok(config_path)
}
Ok(res)
}
/// Searches topgrade.d for additional config files
fn ensure_topgrade_d(config_directory: &Path) -> Result<Vec<PathBuf>> {
let mut res = Vec::new();
let dir_to_search = config_directory.join("topgrade.d");
if dir_to_search.exists() {
for entry in fs::read_dir(dir_to_search)? {
let entry = entry?;
if entry.file_type()?.is_file() {
debug!(
"Found additional (directory) configuration file at {}",
entry.path().display()
);
res.push(entry.path());
}
}
res.sort();
} else {
debug!("No additional configuration directory exists, creating one");
fs::create_dir_all(&dir_to_search)?;
}
Ok(res)
}
/// Read the configuration file.
///
/// If the configuration file does not exist the function returns the default ConfigFile.
/// If the configuration file does not exist, the function returns the default ConfigFile.
fn read(config_path: Option<PathBuf>) -> Result<ConfigFile> {
let mut result = Self::default();
let config_path = if let Some(path) = config_path {
path
} else {
Self::ensure()?
let (path, dir_include) = Self::ensure()?;
/*
The Function was called without a config_path, we need
to read the include directory before returning the main config path
*/
for include in dir_include {
let include_contents = fs::read_to_string(&include).map_err(|e| {
tracing::error!("Unable to read {}", include.display());
e
})?;
let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| {
tracing::error!("Failed to deserialize {}", include.display());
e
})?;
result.merge(include_contents_parsed);
}
path
};
let contents = fs::read_to_string(&config_path).map_err(|e| {
let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| {
tracing::error!("Unable to read {}", config_path.display());
e
})?;
let mut result: Self = toml::from_str(&contents).map_err(|e| {
tracing::error!("Failed to deserialize {}", config_path.display());
Self::ensure_misc_is_present(&mut contents_non_split, &config_path);
// To parse [include] sections in the order as they are written,
// we split the file and parse each part as a separate file
let regex_match_include = Regex::new(r"\[include]").expect("Failed to compile regex");
let contents_split = regex_match_include.split_inclusive_left(contents_non_split.as_str());
for contents in contents_split {
let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).map_err(|e| {
tracing::error!("Failed to deserialize an include section of {}", config_path.display());
e
})?;
if let Some(ref mut paths) = &mut result.git_repos {
if let Some(includes) = &config_file_include_only.include {
// Parses the [include] section present in the slice
for include in includes.paths.iter().rev() {
let include_path = shellexpand::tilde::<&str>(&include.as_ref()).into_owned();
let include_path = PathBuf::from(include_path);
let include_contents = match fs::read_to_string(&include_path) {
Ok(c) => c,
Err(e) => {
tracing::error!("Unable to read {}: {}", include_path.display(), e);
continue;
}
};
match toml::from_str::<Self>(&include_contents) {
Ok(include_parsed) => result.merge(include_parsed),
Err(e) => {
tracing::error!("Failed to deserialize {}: {}", include_path.display(), e);
continue;
}
};
debug!("Configuration include found: {}", include_path.display());
}
}
match toml::from_str::<Self>(contents) {
Ok(contents) => result.merge(contents),
Err(e) => tracing::error!("Failed to deserialize {}: {}", config_path.display(), e),
}
}
if let Some(misc) = &mut result.misc {
if let Some(ref mut paths) = &mut misc.git_repos {
for path in paths.iter_mut() {
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
debug!("Path {} expanded to {}", path, expanded);
*path = expanded;
}
}
}
if let Some(paths) = result.git.as_mut().and_then(|git| git.repos.as_mut()) {
for path in paths.iter_mut() {
@@ -423,7 +674,7 @@ impl ConfigFile {
}
fn edit() -> Result<()> {
let config_path = Self::ensure()?;
let config_path = Self::ensure()?.0;
let editor = editor();
debug!("Editor: {:?}", editor);
@@ -436,6 +687,18 @@ impl ConfigFile {
.status_checked()
.context("Failed to open configuration file editor")
}
/// [Misc] was added later, here we check if it is present in the config file and add it if not
fn ensure_misc_is_present(contents: &mut String, path: &PathBuf) {
if !contents.contains("[misc]") {
debug!("Adding [misc] section to {}", path.display());
string_prepend_str(contents, "[misc]\n");
File::create(path)
.and_then(|mut f| f.write_all(contents.as_bytes()))
.expect("Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError");
}
}
}
// Command line arguments
@@ -565,7 +828,7 @@ impl CommandLineArgs {
/// 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
/// Its provided methods decide the appropriate options based on combining the configuration file and the
/// command line arguments.
pub struct Config {
opt: CommandLineArgs,
@@ -576,7 +839,7 @@ pub struct Config {
impl Config {
/// Load the configuration.
///
/// The function parses the command line arguments and reading the configuration file.
/// The function parses the command line arguments and reads the configuration file.
pub fn load(opt: CommandLineArgs) -> Result<Self> {
let config_directory = config_directory();
let config_file = if config_directory.is_dir() {
@@ -587,15 +850,17 @@ impl Config {
ConfigFile::default()
})
} else {
tracing::debug!("Configuration directory {} does not exist", config_directory.display());
debug!("Configuration directory {} does not exist", config_directory.display());
ConfigFile::default()
};
check_deprecated!(config_file, git_arguments, git, arguments);
check_deprecated!(config_file, git_repos, git, repos);
check_deprecated!(config_file, predefined_git_repos, git, pull_predefined);
check_deprecated!(config_file, yay_arguments, linux, yay_arguments);
check_deprecated!(config_file, accept_all_windows_updates, windows, accept_all_updates);
if let Some(misc) = &config_file.misc {
check_deprecated!(misc, git_arguments, git, arguments);
check_deprecated!(misc, git_repos, git, repos);
check_deprecated!(misc, predefined_git_repos, git, pull_predefined);
check_deprecated!(misc, yay_arguments, linux, yay_arguments);
check_deprecated!(misc, accept_all_windows_updates, windows, accept_all_updates);
}
let allowed_steps = Self::allowed_steps(&opt, &config_file);
@@ -628,7 +893,7 @@ impl Config {
/// The list of additional git repositories to pull.
pub fn git_repos(&self) -> &Option<Vec<String>> {
get_deprecated!(self.config_file, git_repos, git, repos)
get_deprecated_moved_opt!(&self.config_file.misc, git_repos, &self.config_file.git, repos)
}
/// Tell whether the specified step should run.
@@ -643,8 +908,10 @@ impl Config {
let mut enabled_steps: Vec<Step> = Vec::new();
enabled_steps.extend(&opt.only);
if let Some(only) = config_file.only.as_ref() {
enabled_steps.extend(only)
if let Some(misc) = config_file.misc.as_ref() {
if let Some(only) = misc.only.as_ref() {
enabled_steps.extend(only);
}
}
if enabled_steps.is_empty() {
@@ -653,27 +920,47 @@ impl Config {
let mut disabled_steps: Vec<Step> = Vec::new();
disabled_steps.extend(&opt.disable);
if let Some(disabled) = config_file.disable.as_ref() {
if let Some(misc) = config_file.misc.as_ref() {
if let Some(disabled) = misc.disable.as_ref() {
disabled_steps.extend(disabled);
}
}
enabled_steps.retain(|e| !disabled_steps.contains(e) || opt.only.contains(e));
enabled_steps
}
/// Tell whether we should run a self update.
/// Tell whether we should run a self-update.
pub fn no_self_update(&self) -> bool {
self.opt.no_self_update || self.config_file.no_self_update.unwrap_or(false)
self.opt.no_self_update
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.no_self_update)
.unwrap_or(false)
}
/// Tell whether we should run in tmux.
pub fn run_in_tmux(&self) -> bool {
self.opt.run_in_tmux || self.config_file.run_in_tmux.unwrap_or(false)
self.opt.run_in_tmux
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.run_in_tmux)
.unwrap_or(false)
}
/// Tell whether we should perform cleanup steps.
pub fn cleanup(&self) -> bool {
self.opt.cleanup || self.config_file.cleanup.unwrap_or(false)
self.opt.cleanup
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.cleanup)
.unwrap_or(false)
}
/// Tell whether we are dry-running.
@@ -683,32 +970,54 @@ impl Config {
/// Tell whether we should not attempt to retry anything.
pub fn no_retry(&self) -> bool {
self.opt.no_retry || self.config_file.no_retry.unwrap_or(false)
self.opt.no_retry
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.no_retry)
.unwrap_or(false)
}
/// List of remote hosts to run Topgrade in
pub fn remote_topgrades(&self) -> &Option<Vec<String>> {
&self.config_file.remote_topgrades
pub fn remote_topgrades(&self) -> Option<&Vec<String>> {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.remote_topgrades.as_ref())
}
/// Path to Topgrade executable used for all remote hosts
pub fn remote_topgrade_path(&self) -> &str {
self.config_file.remote_topgrade_path.as_deref().unwrap_or("topgrade")
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.remote_topgrade_path.as_deref())
.unwrap_or("topgrade")
}
/// Extra SSH arguments
pub fn ssh_arguments(&self) -> &Option<String> {
&self.config_file.ssh_arguments
pub fn ssh_arguments(&self) -> Option<&String> {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.ssh_arguments.as_ref())
}
/// Extra Git arguments
pub fn git_arguments(&self) -> &Option<String> {
get_deprecated!(self.config_file, git_arguments, git, arguments)
get_deprecated_moved_opt!(&self.config_file.misc, git_arguments, &self.config_file.git, arguments)
}
/// Extra Tmux arguments
pub fn tmux_arguments(&self) -> eyre::Result<Vec<String>> {
let args = &self.config_file.tmux_arguments.as_deref().unwrap_or_default();
pub fn tmux_arguments(&self) -> Result<Vec<String>> {
let args = &self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.tmux_arguments.as_ref())
.map(String::to_owned)
.unwrap_or_default();
shell_words::split(args)
// The only time the parse failed is in case of a missing close quote.
// The error message looks like this:
@@ -726,7 +1035,7 @@ impl Config {
/// Skip sending a notification at the end of a run
pub fn skip_notify(&self) -> bool {
if let Some(yes) = self.config_file.skip_notify {
if let Some(yes) = self.config_file.misc.as_ref().and_then(|misc| misc.skip_notify) {
return yes;
}
@@ -735,12 +1044,16 @@ impl Config {
/// Whether to set the terminal title
pub fn set_title(&self) -> bool {
self.config_file.set_title.unwrap_or(true)
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.set_title)
.unwrap_or(true)
}
/// Whether to say yes to package managers
pub fn yes(&self, step: Step) -> bool {
if let Some(yes) = self.config_file.assume_yes {
if let Some(yes) = self.config_file.misc.as_ref().and_then(|misc| misc.assume_yes) {
return yes;
}
@@ -757,18 +1070,22 @@ impl Config {
/// Bash-it branch
pub fn bashit_branch(&self) -> &str {
self.config_file.bashit_branch.as_deref().unwrap_or("stable")
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.bashit_branch.as_deref())
.unwrap_or("stable")
}
/// Whether to accept all Windows updates
pub fn accept_all_windows_updates(&self) -> bool {
get_deprecated!(
self.config_file,
get_deprecated_moved_or_default_to!(
&self.config_file.misc,
accept_all_windows_updates,
windows,
accept_all_updates
&self.config_file.windows,
accept_all_updates,
true
)
.unwrap_or(true)
}
/// Whether to self rename the Topgrade executable during the run
@@ -836,7 +1153,11 @@ impl Config {
/// Whether to send a desktop notification at the beginning of every step
pub fn notify_each_step(&self) -> bool {
self.config_file.notify_each_step.unwrap_or(false)
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.notify_each_step)
.unwrap_or(false)
}
/// Extra garuda-update arguments
@@ -962,7 +1283,7 @@ impl Config {
self.config_file.git.as_ref().and_then(|git| git.max_concurrency)
}
/// Should we power on vagrant boxes if needed
/// Determine whether we should power on vagrant boxes
pub fn vagrant_power_on(&self) -> Option<bool> {
self.config_file.vagrant.as_ref().and_then(|vagrant| vagrant.power_on)
}
@@ -992,7 +1313,7 @@ impl Config {
.unwrap_or(false)
}
/// Use distro-sync in Red Hat based distrbutions
/// Use distro-sync in Red Hat based distributions
pub fn redhat_distro_sync(&self) -> bool {
self.config_file
.linux
@@ -1019,18 +1340,25 @@ impl Config {
.unwrap_or(false)
}
/// Should we ignore failures for this step
/// Determine if we should ignore failures for this step
pub fn ignore_failure(&self, step: Step) -> bool {
self.config_file
.ignore_failures
.misc
.as_ref()
.and_then(|misc| misc.ignore_failures.as_ref())
.map(|v| v.contains(&step))
.unwrap_or(false)
}
pub fn use_predefined_git_repos(&self) -> bool {
!self.opt.disable_predefined_git_repos
&& get_deprecated!(self.config_file, predefined_git_repos, git, pull_predefined).unwrap_or(true)
&& get_deprecated_moved_or_default_to!(
&self.config_file.misc,
predefined_git_repos,
&self.config_file.git,
pull_predefined,
true
)
}
pub fn verbose(&self) -> bool {
@@ -1056,7 +1384,11 @@ impl Config {
/// If `true`, `sudo` should be called after `pre_commands` in order to elevate at the
/// start of the session (and not in the middle).
pub fn pre_sudo(&self) -> bool {
self.config_file.pre_sudo.unwrap_or(false)
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.pre_sudo)
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
@@ -1150,11 +1482,19 @@ impl Config {
}
pub fn display_time(&self) -> bool {
self.config_file.display_time.unwrap_or(true)
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.display_time)
.unwrap_or(true)
}
pub fn display_preamble(&self) -> bool {
self.config_file.display_preamble.unwrap_or(true)
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.display_preamble)
.unwrap_or(true)
}
pub fn should_run_custom_command(&self, name: &str) -> bool {

View File

@@ -1,12 +1,13 @@
use crate::error::SkipStep;
use color_eyre::eyre::Result;
use std::env;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use color_eyre::eyre::Result;
use tracing::{debug, error};
use crate::error::SkipStep;
pub trait PathExt
where
Self: Sized,
@@ -101,6 +102,13 @@ pub fn require_option<T>(option: Option<T>, cause: String) -> Result<T> {
}
}
pub fn string_prepend_str(string: &mut String, s: &str) {
let mut new_string = String::with_capacity(string.len() + s.len());
new_string.push_str(s);
new_string.push_str(string);
*string = new_string;
}
/* sys-info-rs
*
* Copyright (c) 2015 Siyu Wang
@@ -152,3 +160,56 @@ pub fn hostname() -> Result<String> {
.map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into())
.map(|output| output.stdout.trim().to_owned())
}
pub mod merge_strategies {
use merge::Merge;
use crate::config::Commands;
/// Prepends right to left (both Option<Vec<T>>)
pub fn vec_prepend_opt<T>(left: &mut Option<Vec<T>>, right: Option<Vec<T>>) {
if let Some(left_vec) = left {
if let Some(mut right_vec) = right {
right_vec.append(left_vec);
let _ = std::mem::replace(left, Some(right_vec));
}
} else {
*left = right;
}
}
/// Appends an Option<String> to another Option<String>
pub fn string_append_opt(left: &mut Option<String>, right: Option<String>) {
if let Some(left_str) = left {
if let Some(right_str) = right {
left_str.push(' ');
left_str.push_str(&right_str);
}
} else {
*left = right;
}
}
pub fn inner_merge_opt<T>(left: &mut Option<T>, right: Option<T>)
where
T: Merge,
{
if let Some(ref mut left_inner) = left {
if let Some(right_inner) = right {
left_inner.merge(right_inner);
}
} else {
*left = right;
}
}
pub fn commands_merge_opt(left: &mut Option<Commands>, right: Option<Commands>) {
if let Some(ref mut left_inner) = left {
if let Some(right_inner) = right {
left_inner.extend(right_inner);
}
} else {
*left = right;
}
}
}