mod config; mod git; mod s3; mod sync; use std::path::PathBuf; use std::process::ExitCode; use anyhow::Result; use clap::{Parser, Subcommand}; use crate::config::Config; /// Mirror your Git repositories. #[derive(Parser)] #[command(name = "kalandra", version)] struct Cli { /// The path to the config file. #[arg(long, short, global = true, default_value = "kalandra.toml")] config: PathBuf, #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { /// Syncs repositories to their configured destinations. Sync { /// The names of the repositories to sync. Syncs all repositories if /// none are specified. repos: Vec, }, /// Lists the configured repositories and their destinations. List, } #[tokio::main] async fn main() -> ExitCode { match run().await { Ok(exit_code) => exit_code, Err(err) => { eprintln!("error: {err:#}"); ExitCode::FAILURE } } } async fn run() -> Result { let cli = Cli::parse(); let config = Config::load(&cli.config)?; match cli.command { Command::Sync { repos } => sync(&config, &repos).await, Command::List => { for repo in &config.repos { println!( "{} <- {} -> [{}]", repo.name, repo.source, repo.destinations.join(", ") ); } Ok(ExitCode::SUCCESS) } } } async fn sync(config: &Config, repo_names: &[String]) -> Result { let repos = if repo_names.is_empty() { config.repos.iter().collect::>() } else { let mut repos = Vec::new(); for name in repo_names { let repo = config .repos .iter() .find(|repo| &repo.name == name) .ok_or_else(|| anyhow::anyhow!("unknown repo: {name:?}"))?; repos.push(repo); } repos }; let cache_dir = config.cache_dir()?; let mut failures = Vec::new(); for repo in repos { if let Err(err) = crate::sync::sync_repo(config, repo, &cache_dir).await { eprintln!("[{}] error: {err:#}", repo.name); failures.push(repo.name.clone()); } } if !failures.is_empty() { eprintln!("failed to sync: {}", failures.join(", ")); return Ok(ExitCode::FAILURE); } Ok(ExitCode::SUCCESS) }