#![deny(missing_docs)]
mod ansi;
mod config;
pub mod config_files;
mod doctor;
pub mod error;
pub mod git;
pub mod init;
pub mod line_prompter;
pub mod output;
pub mod pkg_list;
pub mod pkg_manager;
pub mod prompt;
pub mod schema;
pub(crate) mod utils;
use std::io::IsTerminal;
use clap::Subcommand;
use xshell::Shell;
use crate::{
config::Config,
config_files::{ConfigFileDirs, ConfigFiles},
error::Error,
git::{Git, GitCmd},
output::Output,
prompt::{DryRunPrompter, PreApplyDecision, Prompter, TerminalPrompter, YesPrompter},
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "lower")]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
impl ColorChoice {
pub fn enabled(self, stream_is_terminal: bool) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::Auto => std::env::var_os("NO_COLOR").is_none() && stream_is_terminal,
}
}
}
#[derive(clap::Args, Debug)]
pub struct Args {
#[clap(long, global = true, value_enum, default_value_t = ColorChoice::Auto)]
pub color: ColorChoice,
#[clap(skip)]
pub stdin_is_terminal: bool,
}
#[derive(Subcommand, Debug)]
pub enum Cmd {
Apply {
#[clap(long, short)]
pull_config: bool,
#[clap(long, short = 'y', conflicts_with = "dry_run")]
yes: bool,
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long)]
allow_dirty: bool,
},
Status {
#[clap(long, short = 'd')]
diff: bool,
#[clap(long, short = 'a')]
all: bool,
},
Pkg {
#[clap(value_name = "PATTERN")]
pattern: Vec<String>,
#[clap(long)]
all: bool,
#[clap(long)]
all_hints: bool,
#[clap(long, short)]
verbose: bool,
},
Repo {
#[command(subcommand)]
command: GitCmd,
},
Init {
url: Option<String>,
#[clap(long, short, requires = "url")]
branch: Option<String>,
#[clap(long)]
apply: bool,
#[clap(long, short = 'y', requires = "apply")]
yes: bool,
},
Doctor,
Schema,
Completions {
shell: clap_complete::Shell,
},
}
struct CommandContext<'dirs> {
sh: Shell,
config: Config<'dirs>,
}
impl<'dirs> CommandContext<'dirs> {
fn load(dirs: &'dirs ConfigFileDirs, pull_config: bool) -> Result<Self, Error> {
let sh = Shell::new()?;
let config = Config::load(dirs, &sh, pull_config)?;
Ok(Self { sh, config })
}
}
fn build_prompter(
yes: bool,
dry_run: bool,
color: bool,
stdin_is_terminal: bool,
) -> Result<Box<dyn Prompter>, Error> {
if dry_run {
Ok(Box::new(DryRunPrompter::new(color)))
} else if yes {
Ok(Box::new(YesPrompter))
} else if stdin_is_terminal {
Ok(Box::new(TerminalPrompter::new(color)?))
} else {
Err(Error::ApplyNeedsYesOrTty)
}
}
pub fn real_main(
args: &Args,
command: &Cmd,
dirs: &ConfigFileDirs,
output: &mut dyn Output,
) -> Result<(), Error> {
match command {
Cmd::Completions { .. } => Ok(()),
Cmd::Init {
url,
branch,
apply,
yes,
} => init::run(
url.as_deref(),
branch.as_deref(),
*apply,
*yes,
dirs,
args,
output,
),
Cmd::Doctor => {
let sh = Shell::new()?;
doctor::run(args, dirs, &sh, output)
}
Cmd::Schema => schema::run(&mut std::io::stdout().lock()),
Cmd::Apply {
pull_config,
yes,
dry_run,
allow_dirty,
} => {
let ctx = CommandContext::load(dirs, *pull_config)?;
let mut config_files = ConfigFiles::new(dirs);
let stdout_color = args.color.enabled(std::io::stdout().is_terminal());
let mut prompter =
build_prompter(*yes, *dry_run, stdout_color, args.stdin_is_terminal)?;
ctx.config.push_pkg_health(output)?;
let git = Git::new(dirs.zenops(), &ctx.sh);
if git.is_git_repo()? && git.has_uncommitted_changes()? {
ctx.config.check_own_status(&ctx.sh, output)?;
if *yes && !*allow_dirty {
return Err(Error::DirtyRepoRequiresAllowDirty(
dirs.zenops().to_path_buf(),
));
}
if !*allow_dirty {
git.print_pre_apply_summary(stdout_color)?;
match prompter.confirm_pre_apply()? {
PreApplyDecision::CommitAndPush { message } => {
git.commit_all_and_push(&message)?;
}
PreApplyDecision::Continue => {}
PreApplyDecision::Abort => return Ok(()),
}
}
}
ctx.config.update_config_files(&ctx.sh, &mut config_files)?;
config_files.apply_changes(output, prompter.as_mut())?;
Ok(())
}
Cmd::Status { diff: _, all: _ } => {
let ctx = CommandContext::load(dirs, false)?;
let mut config_files = ConfigFiles::new(dirs);
ctx.config.push_pkg_health(output)?;
ctx.config.check_own_status(&ctx.sh, output)?;
ctx.config.update_config_files(&ctx.sh, &mut config_files)?;
config_files.check_status(output)?;
Ok(())
}
Cmd::Pkg {
pattern,
all,
all_hints,
verbose,
} => {
let ctx = CommandContext::load(dirs, false)?;
pkg_list::push(
&ctx.config,
pkg_list::Options {
pattern: pattern.clone(),
all: *all,
all_hints: *all_hints,
verbose: *verbose,
},
output,
)?;
Ok(())
}
Cmd::Repo { command } => {
let sh = Shell::new()?;
command.passthru_dispatch_in(dirs.zenops(), &sh)?;
Ok(())
}
}
}