use std::fmt::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use std::str::FromStr;
use std::sync::Mutex;
use anstream::{ColorChoice, StripStream, eprintln};
use anyhow::{Context, Result};
use clap::{CommandFactory, Parser};
use clap_complete::CompleteEnv;
use owo_colors::OwoColorize;
use prek_consts::env_vars::EnvVars;
use tracing::debug;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::filter::Directive;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer};
use crate::cleanup::cleanup;
use crate::cli::{
CacheCommand, CacheNamespace, Cli, Command, ExitStatus, UtilCommand, UtilNamespace, flag,
};
#[cfg(feature = "self-update")]
use crate::cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
use crate::printer::Printer;
use crate::run::USE_COLOR;
use crate::store::Store;
mod archive;
mod cleanup;
mod cli;
mod config;
mod fs;
mod git;
mod hook;
mod hook_entry;
mod hooks;
mod http;
mod install_source;
mod languages;
mod printer;
mod process;
#[cfg(all(unix, feature = "profiler"))]
mod profiler;
#[cfg(unix)]
mod resource_limit;
mod run;
#[cfg(feature = "schemars")]
mod schema;
mod store;
mod version;
mod warnings;
mod workspace;
mod yaml;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Level {
#[default]
Default,
Verbose,
Debug,
Trace,
TraceAll,
}
enum LogFile {
Default,
Path(PathBuf),
Disabled,
}
impl LogFile {
fn from_args(log_file: Option<PathBuf>, no_log_file: bool) -> Self {
if no_log_file {
Self::Disabled
} else if let Some(path) = log_file {
Self::Path(path)
} else {
Self::Default
}
}
fn is_disabled(&self) -> bool {
matches!(self, Self::Disabled)
}
}
fn setup_logging(level: Level, log_file: LogFile, store: &Store) -> Result<()> {
let directive = match level {
Level::Default | Level::Verbose => LevelFilter::OFF.into(),
Level::Debug => Directive::from_str("prek=debug")?,
Level::Trace => Directive::from_str("prek=trace")?,
Level::TraceAll => Directive::from_str("trace")?,
};
let stderr_filter = EnvFilter::builder()
.with_default_directive(directive)
.from_env()
.context("Invalid RUST_LOG directive")?;
let stderr_format = tracing_subscriber::fmt::format()
.with_target(false)
.with_ansi(*USE_COLOR);
let stderr_layer = tracing_subscriber::fmt::layer()
.with_span_events(FmtSpan::CLOSE)
.event_format(stderr_format)
.with_writer(anstream::stderr)
.with_ansi_sanitization(false)
.with_filter(stderr_filter);
let registry = tracing_subscriber::registry().with(stderr_layer);
if log_file.is_disabled() {
registry.init();
} else {
let log_file_path = match log_file {
LogFile::Default => store.log_file(),
LogFile::Path(path) => path,
LogFile::Disabled => unreachable!(),
};
let log_file = fs_err::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(log_file_path)
.context("Failed to open log file")?;
let log_file = Mutex::new(StripStream::new(log_file.into_file()));
let file_format = tracing_subscriber::fmt::format()
.with_target(false)
.with_ansi(false);
let file_layer = tracing_subscriber::fmt::layer()
.with_span_events(FmtSpan::CLOSE)
.event_format(file_format)
.with_writer(log_file)
.with_filter(EnvFilter::new("prek=trace"));
registry.with(file_layer).init();
}
Ok(())
}
async fn run(cli: Cli) -> Result<ExitStatus> {
let _ = anstyle_query::windows::enable_ansi_colors();
ColorChoice::write_global(cli.globals.color.into());
let store = Store::from_settings()?;
let log_file = LogFile::from_args(cli.globals.log_file.clone(), cli.globals.no_log_file);
setup_logging(
match cli.globals.verbose {
0 => Level::Default,
1 => Level::Verbose,
2 => Level::Debug,
3 => Level::Trace,
_ => Level::TraceAll,
},
log_file,
&store,
)?;
let printer = if cli.globals.quiet == 1 {
Printer::Quiet
} else if cli.globals.quiet > 1 {
Printer::Silent
} else if cli.globals.verbose > 1 {
Printer::Verbose
} else if cli.globals.no_progress {
Printer::NoProgress
} else {
Printer::Default
};
if cli.globals.quiet > 0 {
warnings::disable();
} else {
warnings::enable();
}
debug!("prek: {}", version::version());
#[cfg(unix)]
match resource_limit::adjust_open_file_limit() {
Ok(_) | Err(resource_limit::OpenFileLimitError::AlreadySufficient { .. }) => {}
Err(err) => {
tracing::warn!("Failed to adjust open file limit: {err}");
}
}
if EnvVars::is_set(EnvVars::GIT_DIR) && !EnvVars::is_set(EnvVars::GIT_WORK_TREE) {
let cwd = std::env::current_dir().context("Failed to get current directory")?;
debug!("Setting {} to `{}`", EnvVars::GIT_WORK_TREE, cwd.display());
unsafe { std::env::set_var(EnvVars::GIT_WORK_TREE, cwd) }
}
if let Some(dir) = cli.globals.cd.as_ref() {
debug!("Changing current directory to: `{}`", dir.display());
std::env::set_current_dir(dir)?;
}
debug!("Args: {:?}", std::env::args().collect::<Vec<_>>());
macro_rules! show_settings {
($arg:expr) => {
if cli.globals.show_settings {
writeln!(printer.stdout(), "{:#?}", $arg)?;
return Ok(ExitStatus::Success);
}
};
($arg:expr, false) => {
if cli.globals.show_settings {
writeln!(printer.stdout(), "{:#?}", $arg)?;
}
};
}
show_settings!(cli.globals, false);
let command = cli
.command
.unwrap_or_else(|| Command::Run(Box::new(cli.run_args)));
match command {
Command::Install(args) => {
show_settings!(args);
cli::install(
&store,
cli.globals.config,
args.includes,
args.skips,
args.hook_types,
args.prepare_hooks,
args.overwrite,
args.allow_missing_config,
cli.globals.refresh,
cli.globals.quiet,
cli.globals.verbose,
cli.globals.no_progress,
printer,
args.git_dir.as_deref(),
)
.await
}
Command::PrepareHooks(args) => {
cli::prepare_hooks(
&store,
cli.globals.config,
args.includes,
args.skips,
cli.globals.refresh,
printer,
)
.await
}
Command::Uninstall(args) => {
show_settings!(args);
cli::uninstall(
cli.globals.config,
args.hook_types,
args.all,
printer,
args.git_dir.as_deref(),
)
.await
}
Command::Run(args) => {
show_settings!(args);
cli::run(
&store,
cli.globals.config,
args.includes,
args.skips,
args.stage,
args.from_ref,
args.to_ref,
args.all_files,
args.files,
args.directory,
args.last_commit,
args.show_diff_on_failure,
flag(args.fail_fast, args.no_fail_fast),
args.dry_run,
cli.globals.refresh,
args.extra,
cli.globals.verbose > 0,
printer,
)
.await
}
Command::List(args) => {
show_settings!(args);
cli::list(
&store,
cli.globals.config,
args.includes,
args.skips,
args.hook_stage,
args.language,
args.output_format,
cli.globals.refresh,
cli.globals.verbose > 0,
printer,
)
.await
}
Command::HookImpl(args) => {
show_settings!(args);
cli::hook_impl(
&store,
cli.globals.config,
args.includes,
args.skips,
args.hook_type,
args.hook_dir,
args.skip_on_missing_config,
args.script_version,
args.args,
printer,
)
.await
}
Command::Cache(CacheNamespace {
command: cache_command,
}) => match cache_command {
CacheCommand::Clean => cli::cache_clean(&store, printer),
CacheCommand::Dir => {
writeln!(
printer.stdout_important(),
"{}",
store.path().display().cyan()
)?;
Ok(ExitStatus::Success)
}
CacheCommand::GC(args) => {
cli::cache_gc(&store, args.dry_run, cli.globals.verbose > 0, printer).await
}
CacheCommand::Size(cli::SizeArgs { human }) => cli::cache_size(&store, human, printer),
},
Command::Clean => cli::cache_clean(&store, printer),
Command::GC(args) => {
cli::cache_gc(&store, args.dry_run, cli.globals.verbose > 0, printer).await
}
Command::ValidateConfig(args) => {
show_settings!(args);
cli::validate_configs(args.configs, printer)
}
Command::ValidateManifest(args) => {
show_settings!(args);
cli::validate_manifest(args.manifests, printer)
}
Command::SampleConfig(args) => cli::sample_config(args.file.into(), args.format, printer),
Command::AutoUpdate(args) => {
cli::auto_update(
&store,
cli.globals.config,
args.repo,
args.exclude_repo,
args.include_tag,
args.exclude_tag,
args.repo_include_tag,
args.repo_exclude_tag,
cli.globals.verbose > 0,
args.bleeding_edge,
args.freeze,
args.jobs,
args.dry_run || args.check,
args.exit_code || args.check,
args.cooldown_days,
printer,
)
.await
}
Command::TryRepo(args) => {
show_settings!(args);
cli::try_repo(
cli.globals.config,
args.repo,
args.rev,
args.run_args,
cli.globals.refresh,
cli.globals.verbose > 0,
printer,
)
.await
}
Command::Util(UtilNamespace { command }) => match command {
UtilCommand::Identify(args) => {
show_settings!(args);
cli::identify(&args.paths, args.output_format, printer)
}
UtilCommand::ListBuiltins(args) => {
show_settings!(args);
cli::list_builtins(args.output_format, cli.globals.verbose > 0, printer)
}
UtilCommand::InitTemplateDir(args) => {
show_settings!(args);
cli::init_template_dir(
&store,
args.directory,
cli.globals.config,
args.hook_types,
args.no_allow_missing_config,
cli.globals.refresh,
cli.globals.quiet,
cli.globals.verbose,
cli.globals.no_progress,
printer,
)
.await
}
UtilCommand::YamlToToml(args) => {
show_settings!(args);
cli::yaml_to_toml(args.input, args.output, args.force, printer)
}
UtilCommand::GenerateShellCompletion(args) => {
show_settings!(args);
let mut command = Cli::command();
let bin_name = command
.get_bin_name()
.unwrap_or_else(|| command.get_name())
.to_owned();
clap_complete::generate(args.shell, &mut command, bin_name, &mut std::io::stdout());
Ok(ExitStatus::Success)
}
},
#[cfg(feature = "self-update")]
Command::Self_(SelfNamespace {
command:
SelfCommand::Update(SelfUpdateArgs {
target_version,
token,
}),
}) => cli::self_update(target_version, token, printer).await,
#[cfg(not(feature = "self-update"))]
Command::Self_(_) => {
use crate::install_source::InstallSource;
let msg = InstallSource::detect()
.map(|s| {
format!(
"prek was installed via {} and cannot self-update. To update, run `{}`",
s.description(),
s.update_instructions()
)
})
.unwrap_or_else(|| {
"prek was installed via an external package manager and cannot self-update. \
Please use your package manager to update prek."
.into()
});
anyhow::bail!("{msg}");
}
Command::InitTemplateDir(args) => {
show_settings!(args);
cli::init_template_dir(
&store,
args.directory,
cli.globals.config,
args.hook_types,
args.no_allow_missing_config,
cli.globals.refresh,
cli.globals.quiet,
cli.globals.verbose,
cli.globals.no_progress,
printer,
)
.await
}
}
}
fn main() -> ExitCode {
CompleteEnv::with_factory(Cli::command).complete();
ctrlc::set_handler(move || {
cleanup();
#[allow(clippy::exit, clippy::cast_possible_wrap)]
std::process::exit(if cfg!(windows) {
0xC000_013A_u32 as i32
} else {
130
});
})
.expect("Error setting Ctrl-C handler");
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => err.exit(),
};
#[cfg(all(unix, feature = "profiler"))]
let _profiler_guard = profiler::start_profiling();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime");
let result = runtime.block_on(Box::pin(run(cli)));
runtime.shutdown_background();
#[cfg(all(unix, feature = "profiler"))]
profiler::finish_profiling(_profiler_guard);
match result {
Ok(code) => code.into(),
Err(err) => {
let mut causes = err.chain();
eprintln!("{}: {}", "error".red().bold(), causes.next().unwrap());
for err in causes {
eprintln!(" {}: {}", "caused by".red().bold(), err);
}
ExitStatus::Error.into()
}
}
}