diff --git a/Cargo.lock b/Cargo.lock index c41a9b0..a3284c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -539,6 +548,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c090c608233d81bd6b90e718cf34506c60a10e633dff2292c3d1029e798d669b" +dependencies = [ + "quit_macros", +] + +[[package]] +name = "quit_macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d4b27a0dd5d08ad7af2d17952fb360ec9c30eeade0b32df7a3c9b099ff37564" + [[package]] name = "quote" version = "1.0.40" @@ -553,9 +577,11 @@ name = "refractr" version = "0.3.1" dependencies = [ "clap", + "colored", "ctrlc", "git2", "hex", + "quit", "serde", "serde_derive", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 7371af9..bc0f47e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,11 @@ edition = "2021" [dependencies] clap = { version = "4.5.29", features = ["derive"] } +colored = "3.0.0" ctrlc = "3.4.5" git2 = "0.20.0" hex = "0.4.3" +quit = "2.0.0" serde = "1.0.217" serde_derive = "1.0.217" sha2 = "0.10.8" @@ -16,4 +18,4 @@ toml = "0.8.20" username = "0.2.0" [target.'cfg(target_family = "unix")'.dependencies] -users = "0.11.0" \ No newline at end of file +users = "0.11.0" diff --git a/src/common.rs b/src/common.rs index 4296567..85b6f0a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,13 +1,25 @@ +use colored::Colorize; -#[macro_export] -macro_rules! freak_out { - ($msg:expr) => { - return Err($msg) - }; +pub enum ExitCode { + ParseError = 2, + FilesystemError = 3, + RepositoryError = 4, + RemoteError = 5, + PushError = 6 +} + +pub struct ReturnData { + pub code: ExitCode, + pub msg: String, +} + +pub fn error(msg: String, code: ExitCode) { + eprintln!("{} {}", "error:".red().bold(), msg); + quit::with_code(code as u8) } pub fn warning(msg: String) { - eprintln!("warning: {}", msg) + eprintln!("{} {}", "warning:".yellow().bold(), msg) } pub fn verbose(level: u8, msg_lvl: u8, msg: String) { @@ -18,5 +30,5 @@ pub fn verbose(level: u8, msg_lvl: u8, msg: String) { } prefix += "> "; - eprintln!("{}{}", prefix, msg); + eprintln!("{}{}", prefix.purple().bold(), msg); } diff --git a/src/config.rs b/src/config.rs index 1c76a6f..87bc1a1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -107,24 +107,32 @@ pub fn read_config(paths: Vec, refractr: &Refractr) -> Result return Err(format!("refractr: unable to open {}: {}", path.as_path().display(), e)), + Err(e) => return Err(format!("unable to open {}: {}", path.as_path().display(), e)), Ok(file) => file }; if let Err(e) = file.read_to_string(&mut data) { - return Err(format!("refractr: unable to read {}: {}", path.as_path().display(), e)) + return Err(format!("unable to read {}: {}", path.as_path().display(), e)) } + let config: Config = match toml::from_str(&data) { + Ok(c) => c, + Err(e) => return Err(format!("issues parsing toml file {}: {}", path.as_path().display(), e)) + }; + let config_file = ConfigFile { path: match fs::canonicalize(&path) { - Err(_) => return Err(format!("refractr: cannot get absolute path of config file: {}", path.as_path().display())), + Err(_) => return Err(format!("cannot get absolute path of config file: {}", path.as_path().display())), Ok(abs) => abs.to_string_lossy().to_string() }, file: match fs::metadata(&path) { - Err(_) => return Err(format!("refractr: cannot obtain metadata for config file: {}", path.as_path().display())), + Err(_) => return Err(format!("cannot obtain metadata for config file: {}", path.as_path().display())), Ok(metadata) => metadata }, - config: verify_config(toml::from_str(&data).unwrap()) + config: match verify_config(&config) { + Err(e) => return Err(format!("invalid config {}: {}", path.as_path().display(), e)), + Ok(_) => config + } }; let mut dup = false; @@ -146,25 +154,37 @@ pub fn read_config(paths: Vec, refractr: &Refractr) -> Result Config { +fn verify_config(config: &Config) -> Result<(), String> { if config.schedule.enabled { - assert_ne!(config.schedule.interval, None); - assert!(config.schedule.interval.unwrap() >= 60); + match config.schedule.interval { + Some(i) => { + if i < 60 { + return Err(format!("schedule is enabled, but less than 60")) + } + }, + None => return Err(format!("schedule is enabled, but no interval was defined")) + } } - assert_ne!("", match &config.work_dir { + match &config.work_dir { Some(path) => format!("{}", path), None => { if cfg!(windows) { match env::var("TEMP") { Ok(val) => val, - Err(_) => format!("") + Err(_) => return Err(format!("cannot determine the default temp dir")) } } else { format!("/tmp/refractr") } }, - }); + }; - return config; + if !&config.from.starts_with("ssh://") + && !&config.from.starts_with("https://") + && !&config.from.starts_with("http://") { + return Err(format!("'from' value does not use a supported protocol")) + } + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index e89a3c3..99f9b5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,7 @@ fn get_config_default() -> &'static str { return "/usr/local/etc/refractr/config.toml"; } +#[quit::main] fn main() -> Result<(), String> { let args = Args::parse(); let refractr = Refractr { @@ -77,29 +78,35 @@ fn main() -> Result<(), String> { Err(_) => common::warning(format!("failed to get process username")), } - common::verbose(refractr.verbose, 1, format!("Level {} verbosity enabled", refractr.verbose.to_string())); + common::verbose(refractr.verbose, 1, format!("refractr started with level {} verbosity enabled", refractr.verbose.to_string())); common::verbose(refractr.verbose, 3, format!("Process ID: {}", refractr.pid)); common::verbose(refractr.verbose, 3, format!("Running in Docker: {}", refractr.docker)); + common::verbose(refractr.verbose, 3, format!("System is UNIX(-like): {}", refractr.unix)); common::verbose(refractr.verbose, 2, format!("Checking for create flag")); if args.create { common::verbose(refractr.verbose, 3, format!("Printing sample config")); let example = include_str!("example/config.toml"); println!("{}", example); - Ok(()) - } else { - let cfgs = match config::read_config(args.config, &refractr) { - Ok(cfgs) => cfgs, - Err(e) => freak_out!(e) - }; - if refractr.verbose >= 2 { - // no need to loop over configs if verbose is not at the correct level - for i in &cfgs { - common::verbose(refractr.verbose, 2, format!("{}", i)); - } - } - - common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully")); - refractr.run(cfgs) + return Ok(()) } + let cfgs = match config::read_config(args.config, &refractr) { + Ok(cfgs) => cfgs, + Err(e) => return Err(e) + }; + if refractr.verbose >= 2 { + // no need to loop over configs if verbose is not at the correct level + for i in &cfgs { + common::verbose(refractr.verbose, 2, format!("{}", i)); + } + } + + common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully")); + match refractr.run(cfgs) { + Ok(_) => (), + Err(e) => common::error(format!("{}", e.msg), e.code) + }; + + Ok(()) + } diff --git a/src/refractr.rs b/src/refractr.rs index 6863f0f..3266580 100644 --- a/src/refractr.rs +++ b/src/refractr.rs @@ -1,10 +1,9 @@ -use crate::freak_out; -use crate::common; +use crate::common::{self, ReturnData, ExitCode}; use crate::config::{Config, ConfigFile}; use git2::{FetchOptions, CertificateCheckStatus, Cred, PushOptions, RemoteCallbacks, Repository}; use git2::{Error, ErrorCode}; -use git2::build::{RepoBuilder, CheckoutBuilder}; +use git2::build::RepoBuilder; use hex; use sha2::{Sha256, Digest}; use std::env; @@ -33,7 +32,7 @@ struct OpenedRepository { impl Refractr { fn set_up_work_dir(&self, work_dir: PathBuf) -> Result { if let Err(e) = fs::create_dir_all(&work_dir) { - freak_out!(format!("could not create working directory: {}: {}", work_dir.to_string_lossy().to_string(), e)) + return Err(format!("could not create working directory: {}: {}", work_dir.to_string_lossy().to_string(), e)) } Ok(work_dir.to_string_lossy().to_string()) } @@ -121,7 +120,7 @@ impl Refractr { common::warning(format!("remote {} already exists, skipping", remote_id)); remote_list.push(remote_id) } else { - freak_out!(format!("failed to create remote: {}", e)); + return Err(format!("failed to create remote: {}", e)); } } } @@ -180,7 +179,7 @@ impl Refractr { Ok(()) } - fn looper(&self, repos: Vec) -> Result<(), String> { + fn looper(&self, repos: Vec) -> Result<(), ReturnData> { let mut current_ints = Vec::new(); let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); @@ -224,8 +223,8 @@ impl Refractr { &repos[i].cfg, &repos[i].repo, &repos[i].remotes) { - return Err(e) - } + common::error(e, ExitCode::PushError) + }; } } do_break = false; @@ -238,7 +237,7 @@ impl Refractr { Ok(()) } - pub fn run(&self, cfgs: Vec) -> Result<(), String> { + pub fn run(&self, cfgs: Vec) -> Result<(), ReturnData> { common::verbose(self.verbose, 3, format!("Starting main refractr loop")); let mut loop_repos = Vec::new(); @@ -248,7 +247,7 @@ impl Refractr { let work_dir = self.set_up_work_dir(match &cfg.config.work_dir { None => { if cfg!(windows) { - PathBuf::from(format!("{}\\refractr\"", env::var("TEMP").unwrap())) + PathBuf::from(format!("{}\\refractr", env::var("TEMP").unwrap())) } else { PathBuf::from("/tmp/refractr") } @@ -257,7 +256,10 @@ impl Refractr { }); let path_str = match work_dir { Ok(p) => p, - Err(e) => return Err(e) + Err(e) => return Err(ReturnData { + code: ExitCode::FilesystemError, + msg: e + }) }; common::verbose( self.verbose, @@ -265,19 +267,17 @@ impl Refractr { format!("Created working directory: {}", &path_str)); let repo_name = match &cfg.config.from.split("/").last() { Some(split) => split.to_string(), - None => freak_out!(format!("failed to parse repository name")) + None => return Err(ReturnData { + code: ExitCode::ParseError, + msg: format!("failed to parse repository name") + }) }; - // make initial clone - common::verbose( - self.verbose, - 1, - format!("Cloning repository: {}", &cfg.config.from)); - let mut builder = RepoBuilder::new(); let mut cb = RemoteCallbacks::new(); let mut fo = FetchOptions::new(); + // make initial clone if cfg.config.from.starts_with("ssh://") { let key_string = cfg.config.git.ssh_identity_file.clone(); cb.credentials(move |_,_,_| Cred::ssh_key( @@ -304,17 +304,28 @@ impl Refractr { builder.fetch_options(fo); let repo_dir = format!("{}{}{}", &path_str, MAIN_SEPARATOR_STR, repo_name); + common::verbose( + self.verbose, + 1, + format!("Cloning repository {} to {}", &cfg.config.from, &repo_dir)); let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) { Ok(repo) => repo, - Err(_) => { + Err(e) => { + println!("{}", e); common::warning(format!("found existing repo at {}, attempting to use", repo_dir)); match self.fast_forward(&repo_dir, &cfg.config.branches) { Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) { repo } else { - freak_out!(format!("failed to obtain existing repo")) + return Err(ReturnData { + code: ExitCode::RepositoryError, + msg: format!("failed to obtain existing repo") + }) }, - Err(e) => freak_out!(format!("failed to obtain existing repo: {}", e)) + Err(e) => return Err(ReturnData { + code: ExitCode::RepositoryError, + msg: format!("failed to obtain existing repo: {}", e) + }) } } }; @@ -324,10 +335,13 @@ impl Refractr { let remotes = match self.make_remotes(&repo, &cfg) { Ok(v) => v, - Err(e) => return Err(e) + Err(e) => return Err(ReturnData { + code: ExitCode::RemoteError, + msg: e + }) }; if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) { - return Err(e) + common::error(e, ExitCode::PushError); } if cfg.config.schedule.enabled {