use std::error::Error;
use clap::Parser;
use env_logger::Target;
use log::warn;
use protofetch::{DependencyUpdate, LockMode, LockUpdateMode, Protofetch};
#[derive(Debug, Parser)]
#[clap(version)]
pub struct CliArgs {
#[clap(subcommand)]
pub cmd: Command,
#[clap(short, long, default_value = "protofetch.toml")]
pub module_location: String,
#[clap(short, long, default_value = "protofetch.lock")]
pub lockfile_location: String,
#[clap(short, long)]
pub cache_directory: Option<String>,
#[clap(short, long)]
pub output_proto_directory: Option<String>,
#[clap(long)]
pub jobs: Option<usize>,
#[clap(long)]
pub copy_jobs: Option<usize>,
}
#[derive(Debug, Parser)]
pub enum Command {
Fetch {
#[clap(long)]
locked: bool,
#[clap(short, long, hide(true))]
force_lock: bool,
},
Lock,
Update {
#[clap(value_name = "DEP")]
deps: Vec<String>,
#[clap(long)]
precise: Option<String>,
},
Init {
#[clap(default_value = ".")]
directory: String,
#[clap(short, long)]
name: Option<String>,
},
Migrate {
#[clap(default_value = ".")]
directory: String,
#[clap(short, long)]
name: Option<String>,
},
Clean,
ClearCache,
}
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.target(Target::Stdout)
.format(move |buf, record| {
use std::io::Write;
let at_least_debug_log = log::log_enabled!(log::Level::Debug);
let level = record.level();
let style = buf.default_level_style(level);
write!(buf, "{style}{}{style:#}", level)?;
if at_least_debug_log {
write!(
buf,
" [{}:{}]",
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
)?;
}
writeln!(buf, " {}", record.args())
})
.init();
if let Err(e) = run() {
log::error!("{}", e);
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn Error>> {
let cli_args: CliArgs = CliArgs::parse();
let mut protofetch = Protofetch::builder()
.module_file_name(&cli_args.module_location)
.lock_file_name(&cli_args.lockfile_location);
if let Some(output_directory_name) = &cli_args.output_proto_directory {
protofetch = protofetch.output_directory_name(output_directory_name)
}
if let Some(cache_directory) = &cli_args.cache_directory {
protofetch = protofetch.cache_directory(cache_directory);
}
if let Some(jobs) = cli_args.jobs {
if jobs == 0 {
return Err("--jobs must be at least 1".into());
}
protofetch = protofetch.jobs(jobs);
}
if let Some(copy_jobs) = cli_args.copy_jobs {
if copy_jobs == 0 {
return Err("--copy-jobs must be at least 1".into());
}
protofetch = protofetch.copy_jobs(copy_jobs);
}
match cli_args.cmd {
Command::Fetch { locked, force_lock } => {
let lock_mode = if force_lock {
warn!("Specifying --force-lock is deprecated, please use \"protofetch update\" instead");
LockMode::Recreate
} else if locked {
LockMode::Locked
} else {
LockMode::Update
};
protofetch.try_build()?.fetch(lock_mode)
}
Command::Lock => protofetch.try_build()?.update(LockUpdateMode::Reconcile),
Command::Update { deps, precise } => {
if precise.is_some() && deps.len() != 1 {
return Err("--precise requires exactly one DEP".into());
}
if deps.is_empty() {
protofetch.try_build()?.update(LockUpdateMode::Full)
} else {
let updates = match precise {
Some(commit_hash) => vec![DependencyUpdate::Precise {
name: deps.into_iter().next().expect("validated one DEP"),
commit_hash,
}],
None => deps
.into_iter()
.map(|name| DependencyUpdate::Latest { name })
.collect(),
};
protofetch
.try_build()?
.update(LockUpdateMode::ReconcileAndUpdate(updates))
}
}
Command::Init { directory, name } => protofetch.root(directory).try_build()?.init(name),
Command::Migrate { directory, name } => protofetch
.root(&directory)
.try_build()?
.migrate(name, directory),
Command::Clean => protofetch.try_build()?.clean(),
Command::ClearCache => protofetch.try_build()?.clear_cache(),
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::{CliArgs, Command};
#[test]
fn update_accepts_positional_specs_and_precise() {
let args =
CliArgs::try_parse_from(["protofetch", "update", "repo1", "--precise", "abc123"])
.unwrap();
match args.cmd {
Command::Update {
deps: specs,
precise,
} => {
assert_eq!(specs, vec!["repo1"]);
assert_eq!(precise.as_deref(), Some("abc123"));
}
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn update_accepts_multiple_positional_specs() {
let args = CliArgs::try_parse_from(["protofetch", "update", "repo1", "repo2"]).unwrap();
match args.cmd {
Command::Update {
deps: specs,
precise,
} => {
assert_eq!(specs, vec!["repo1", "repo2"]);
assert_eq!(precise, None);
}
other => panic!("unexpected command: {other:?}"),
}
}
}