diff --git a/Cargo.lock b/Cargo.lock index dc05b67..cfb0a1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.31" @@ -149,6 +155,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys", +] + [[package]] name = "digest" version = "0.10.7" @@ -456,6 +472,18 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "once_cell" version = "1.20.3" @@ -515,6 +543,7 @@ name = "refractr" version = "0.1.0" dependencies = [ "clap", + "ctrlc", "git2", "hex", "serde", diff --git a/Cargo.toml b/Cargo.toml index b40d6f7..8746bb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] clap = { version = "4.5.29", features = ["derive"] } +ctrlc = "3.4.5" git2 = "0.20.0" hex = "0.4.3" serde = "1.0.217" diff --git a/src/config.rs b/src/config.rs index 1ce9abb..5e3260a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,7 +103,7 @@ pub struct Git { #[derive(Deserialize)] pub struct Schedule { pub enabled: bool, - interval: Option, + pub interval: Option, } pub fn read_config(paths: Vec, refractr: &common::Refractr) -> Vec { @@ -154,6 +154,7 @@ pub fn read_config(paths: Vec, refractr: &common::Refractr) -> Vec Config { if config.schedule.enabled { assert_ne!(config.schedule.interval, None); + assert!(config.schedule.interval.unwrap() >= 15); } assert_ne!("", match &config.work_dir { diff --git a/src/main.rs b/src/main.rs index 5bd1f1b..beba68e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,7 @@ fn main() -> std::io::Result<()> { } common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully")); - refractr::start(refractr, cfgs) + refractr::run(refractr, cfgs) } } diff --git a/src/refractr.rs b/src/refractr.rs index dde7782..7f3998e 100644 --- a/src/refractr.rs +++ b/src/refractr.rs @@ -1,13 +1,23 @@ -use git2::{Cred, PushOptions, Remote, RemoteCallbacks, Repository}; +use git2::{Cred, FetchOptions, PushOptions, Remote, RemoteCallbacks, Repository}; use sha2::{Sha256, Digest}; use crate::common::{self, Refractr}; -use crate::config::ConfigFile; +use crate::config::{Config, ConfigFile}; use std::fs; use std::env; +use std::thread; +use std::time; use std::path::{Path, PathBuf}; use git2::{Error, ErrorCode}; use hex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +struct OpenedRepository { + repo: Repository, + remotes: Vec, + cfg: Config, +} fn set_up_work_dir(work_dir: PathBuf) -> String { if let Err(e) = fs::create_dir_all(&work_dir) { @@ -59,9 +69,122 @@ fn fast_forward(repo_dir: &str, branches: &Option>) -> Result<(), Er Ok(()) } -pub fn start(refractr: Refractr, cfgs: Vec) -> std::io::Result<()> { +fn fetch_origin(repo: &Repository, branches: &Option>) { + let mut origin = repo.find_remote("origin").unwrap(); + let callbacks = RemoteCallbacks::new(); + let mut push_options = FetchOptions::new(); + push_options.remote_callbacks(callbacks); + + let mut refs = Vec::new(); + let strings = get_branches(&repo, &branches, true); + for branch in &strings { + refs.push(branch.as_str()); + } + + if let Err(e) = origin.download(&[] as &[&str], Some(&mut push_options)) { + eprintln!("refractr: failed to fetch origin: {}: {}", origin.url().unwrap(), e) + } +} + +fn make_remotes<'a> (refractr: &Refractr, repo: &'a Repository, cfg: &ConfigFile) -> Vec { + // create remotes for each "to" repo + let mut remote_list = Vec::new(); + for to in &cfg.config.to { + let mut hasher = Sha256::new(); + hasher.update(to); + let remote_id = format!("refractr-{}", &hex::encode(hasher.finalize())[..8]); + common::verbose(refractr.verbose, 2, format!("Attempting to create remote {} for url {}", remote_id, to)); + match repo.remote(remote_id.as_str(), to) { + Ok(_) => remote_list.push(remote_id), + Err(e) => { + if e.code() == ErrorCode::Exists { + eprintln!("refractr: warning: remote {} already exists, skipping", remote_id); + remote_list.push(remote_id) + } else { + panic!("refractr: failed to create remote: {}", e); + } + } + } + } + + remote_list +} + +fn push_remotes(refractr: &Refractr, cfg: &Config, repo: &Repository, remote_list: &Vec) { + for id in remote_list { + let mut remote = repo.find_remote(&id).unwrap(); + common::verbose(refractr.verbose, 1, format!("Pushing to remote: {}", remote.url().unwrap())); + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(|_,_,_| Cred::ssh_key("git", None, &Path::new(&cfg.git.ssh_identity_file), None)); + let mut push_options = PushOptions::new(); + push_options.remote_callbacks(callbacks); + + let mut refs = Vec::new(); + let strings = get_branches(&repo, &cfg.branches, true); + for branch in &strings { + refs.push(branch.as_str()); + } + + match remote.push::<&str>(&refs, Some(&mut push_options)) { + Ok(_) => (), + Err(e) => { + eprintln!("refractr: failed to push to remote: {}: {}", remote.url().unwrap(), e) + } + } + } +} + +fn looper(refractr: Refractr, repos: Vec) { + let mut current_ints = Vec::new(); + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + let count = repos.len(); + for i in 0..repos.len() { + current_ints.push(u64::from(repos[i].cfg.schedule.interval.unwrap().unsigned_abs())); + }; + let mut original_ints = current_ints.clone(); + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }).expect("Failed to set ^C handler"); + + common::verbose(refractr.verbose, 1, format!("Starting scheduled loop")); + let min = *current_ints.iter().min().unwrap(); + let mut do_break = false; + while !do_break { + do_break = true; + let sleep_int = time::Duration::from_secs(min); + let now = time::Instant::now(); + + common::verbose(refractr.verbose, 2, format!("Sleeping for {} seconds", sleep_int.as_secs())); + while running.load(Ordering::SeqCst) { + thread::sleep(time::Duration::from_secs(1)); + if now.elapsed().as_secs() >= sleep_int.as_secs() { + common::verbose(refractr.verbose, 3, format!("Thread has awoken!")); + for i in 0..count { + current_ints[i] -= now.elapsed().as_secs(); + if i <= 0 { + current_ints[i] = original_ints[i].clone(); + common::verbose(refractr.verbose, 2, format!("Interval for {} has arrived, pulling", repos[i].cfg.from)); + fetch_origin(&repos[i].repo, &repos[i].cfg.branches); + common::verbose(refractr.verbose, 2, format!("Pushing {}", repos[i].cfg.from)); + push_remotes(&refractr, &repos[i].cfg, &repos[i].repo, &repos[i].remotes); + } + } + do_break = false; + break + } + } + } + common::verbose(refractr.verbose, 1, format!("Exited looper due to ^C")); + +} + +pub fn run(refractr: Refractr, cfgs: Vec) -> std::io::Result<()> { common::verbose(refractr.verbose, 3, format!("Starting main refractr loop")); - for cfg in &cfgs { + let mut loop_repos = Vec::new(); + + for cfg in cfgs { // set up the working directory common::verbose(refractr.verbose, 3, format!("Loaded config: {}", cfg.path)); let path_str = set_up_work_dir(match &cfg.config.work_dir { @@ -102,45 +225,24 @@ pub fn start(refractr: Refractr, cfgs: Vec) -> std::io::Result<()> { } }; - // create remotes for each "to" repo - let mut remote_list: Vec> = Vec::new(); - for to in &cfg.config.to { - let mut hasher = Sha256::new(); - hasher.update(to); - let remote_id = format!("refractr-{}", &hex::encode(hasher.finalize())[..8]); - common::verbose(refractr.verbose, 2, format!("Attempting to create remote {} for url {}", remote_id, to)); - match repo.remote(remote_id.as_str(), to) { - Ok(r) => remote_list.push(r), - Err(e) => { - if e.code() == ErrorCode::Exists { - eprintln!("refractr: warning: remote {} already exists, skipping", remote_id) - } else { - panic!("refractr: failed to create remote: {}", e); - } - } - } + + let repo_fresh = Repository::open(Path::new(&repo_dir)).unwrap(); + let remotes = make_remotes(&refractr, &repo_fresh, &cfg); + push_remotes(&refractr, &cfg.config, &repo, &remotes); + if cfg.config.schedule.enabled { + loop_repos.push(OpenedRepository { + repo, + remotes, + cfg: cfg.config + }); } + } - for mut remote in remote_list { - common::verbose(refractr.verbose, 1, format!("Pushing to remote: {}", remote.url().unwrap())); - let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(|_,_,_| Cred::ssh_key("git", None, &Path::new(&cfg.config.git.ssh_identity_file), None)); - let mut push_options = PushOptions::new(); - push_options.remote_callbacks(callbacks); - - let mut refs = Vec::new(); - let strings = get_branches(&repo, &cfg.config.branches, true); - for branch in &strings { - refs.push(branch.as_str()); - } - - match remote.push::<&str>(&refs, Some(&mut push_options)) { - Ok(_) => (), - Err(e) => { - eprintln!("refractr: failed to push to remote: {}: {}", remote.url().unwrap(), e) - } - } - } + if loop_repos.len() >= 1 { + common::verbose(refractr.verbose, 2, format!("{} configs have schedules enabled, setting up looper", loop_repos.len())); + looper(refractr, loop_repos); + } else { + common::verbose(refractr.verbose, 2, format!("No scheduled configs found, exiting refractr")); } Ok(())