use git2::build::CheckoutBuilder; use git2::{BranchType, Cred, PushOptions, RemoteCallbacks, Repository}; use sha2::{Sha256, Digest}; use crate::common::{self, Refractr}; 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, path: String, remotes: Vec, cfg: Config, } fn set_up_work_dir(work_dir: PathBuf) -> String { if let Err(e) = fs::create_dir_all(&work_dir) { panic!("refractr: could not create working directory: {}: {}", work_dir.to_string_lossy(), e) } work_dir.to_string_lossy().to_string() } fn get_branches(repo: &Repository, branches: &Option>, refs: bool) -> Vec { match branches { Some(repo_branches) => { if refs { let mut refs_branches = Vec::new(); for branch in repo_branches { refs_branches.push(format!("refs/heads/{}", branch)); } refs_branches } else { repo_branches.to_vec() } }, None => { let mut strings = Vec::new(); let remote_branches = match repo.branches(Some(git2::BranchType::Remote)) { Ok(b) => b, Err(e) => panic!("refractr: failed to get branches: {}", e) }; for branch in remote_branches { if let Ok((b, _)) = branch { if let Ok(Some(name)) = b.name() { if refs { strings.push(format!("refs/heads/{}", name.to_string())) } else { strings.push(name.to_string()); } } } } strings } } } fn fast_forward(refractr: &Refractr, repo_dir: &str, branches: &Option>) -> Result<(), Error> { let repo = Repository::open(repo_dir)?; let branch_list: Vec = get_branches(&repo, branches, false); common::verbose(refractr.verbose, 2, format!("Pulling origin")); repo.find_remote("origin")?.fetch(&branch_list, None, None)?; let fetch_head = repo.find_reference("FETCH_HEAD")?; let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?; let analysis = repo.merge_analysis(&[&fetch_commit])?; if analysis.0.is_fast_forward() { for branch in branch_list { let refname = format!("refs/heads/{}", branch); let mut reference = repo.find_reference(&refname)?; reference.set_target(fetch_commit.id(), "Fast-forward")?; repo.set_head(&refname)?; let _ = repo.checkout_head(Some(CheckoutBuilder::default().force())); } } Ok(()) } 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 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)); let _ = fast_forward(&refractr, &repos[i].path, &repos[i].cfg.branches); 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")); 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 { None => { if cfg!(windows) { PathBuf::from(format!("\"{}\\refractr\"", match env::var("TEMP") { Ok(val) => val, Err(_) => format!("This shouldn't happen!") })) } else { PathBuf::from("/tmp/refractr") } }, Some(path) => PathBuf::from(path) }); common::verbose(refractr.verbose, 2, format!("Created working directory: {}", &path_str)); let repo_name = match &cfg.config.from.split("/").last() { Some(split) => split.to_string(), None => panic!("refractr: failed to parse repository name") }; // make initial clone common::verbose(refractr.verbose, 1, format!("Cloning repository: {}", &cfg.config.from)); let repo_dir = format!("{}/{}", &path_str, repo_name); let repo = match Repository::clone(&cfg.config.from, Path::new(&repo_dir)) { Ok(repo) => repo, Err(_) => { eprintln!("refractr: warning: found existing repo at {}, attempting to use", repo_dir); match fast_forward(&refractr, &repo_dir, &cfg.config.branches) { Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) { repo } else { panic!("refractr: failed to obtain existing repo") }, Err(e) => panic!("refractr: failed to obtain existing repo: {}", 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, path: repo_dir, remotes, cfg: cfg.config }); } } 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(()) }