refractr/src/refractr.rs

359 lines
12 KiB
Rust
Raw Normal View History

2025-03-09 12:16:51 -06:00
use crate::freak_out;
2025-03-05 20:45:35 -07:00
use crate::common;
use crate::config::{Config, ConfigFile};
2025-03-10 20:56:47 -06:00
use git2::{FetchOptions, CertificateCheckStatus, Cred, PushOptions, RemoteCallbacks, Repository};
2025-03-03 21:10:31 -07:00
use git2::{Error, ErrorCode};
2025-03-10 20:56:47 -06:00
use git2::build::{RepoBuilder, CheckoutBuilder};
2025-03-03 21:10:31 -07:00
use hex;
use sha2::{Sha256, Digest};
use std::env;
use std::fs;
2025-03-15 21:06:02 -06:00
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time;
2025-03-05 20:45:35 -07:00
pub struct Refractr {
pub docker: bool,
2025-03-05 20:45:35 -07:00
pub pid: u32,
pub strict: bool,
pub unix: bool,
pub verbose: u8
2025-03-05 20:45:35 -07:00
}
struct OpenedRepository {
repo: Repository,
2025-03-05 00:27:21 -07:00
path: String,
remotes: Vec<String>,
cfg: Config,
}
2025-03-02 18:33:33 -07:00
2025-03-05 20:45:35 -07:00
impl Refractr {
2025-03-09 12:16:51 -06:00
fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<String, String> {
2025-03-05 20:45:35 -07:00
if let Err(e) = fs::create_dir_all(&work_dir) {
2025-03-09 12:16:51 -06:00
freak_out!(format!("could not create working directory: {}: {}", work_dir.to_string_lossy().to_string(), e))
2025-03-05 20:45:35 -07:00
}
2025-03-09 12:16:51 -06:00
Ok(work_dir.to_string_lossy().to_string())
2025-03-02 18:33:33 -07:00
}
2025-03-08 20:48:28 -07:00
fn get_refs(&self, branches: &Vec<String>) -> Vec<String> {
let mut refs_branches = Vec::new();
for branch in branches {
refs_branches.push(format!("refs/heads/{}", branch));
2025-03-02 18:33:33 -07:00
}
2025-03-08 20:48:28 -07:00
refs_branches
2025-03-03 22:54:58 -07:00
}
2025-03-08 20:48:28 -07:00
fn fast_forward(&self, repo_dir: &str, branches: &Vec<String>) -> Result<(), Error> {
2025-03-05 20:45:35 -07:00
let repo = Repository::open(repo_dir)?;
common::verbose(self.verbose, 2, format!("Pulling origin"));
2025-03-08 20:48:28 -07:00
repo.find_remote("origin")?.fetch(&branches, None, None)?;
2025-03-05 20:45:35 -07:00
for branch in branches {
let refname = format!("refs/remotes/origin/{}", branch);
let mut reference = repo.find_reference(&refname)?;
reference.rename(format!("refs/heads/{}", branch).as_str(), true, "")?;
2025-03-05 00:27:21 -07:00
}
2025-03-05 17:20:41 -07:00
2025-03-05 20:45:35 -07:00
Ok(())
}
fn fetch(&self, repo: &Repository, branches: &Vec<String>, ssh: Option<&String>) -> Result<(), Error> {
match ssh {
Some(key) => {
let mut cb = RemoteCallbacks::new();
let mut fo = FetchOptions::new();
cb.credentials(move |_,_,_| Cred::ssh_key(
"git",
None,
Path::new(&key),
None));
cb.certificate_check(|cert, url| {
let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string()));
}
common::warning(
format!("implicitly trusting unknown host {} with sha256 host key {}",
url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec())));
common::warning(
format!("to ignore this error in the future, add this host to your known_hosts file"));
Ok(CertificateCheckStatus::CertificateOk)
});
fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb);
repo.find_remote("origin")?.fetch(&branches, Some(&mut fo), None)?;
},
None => repo.find_remote("origin")?.fetch(&branches, None, None)?
};
Ok(())
}
fn set_up_refs(&self, repo: &Repository, branches: &Vec<String>) -> Result<(), Error> {
for branch in branches {
let mut fetch_head = repo.find_reference(format!("refs/remotes/origin/{}", branch).as_str())?;
fetch_head.rename(format!("refs/heads/{}", branch).as_str(), true, "")?;
}
2025-03-05 17:20:41 -07:00
Ok(())
2025-03-05 20:45:35 -07:00
}
2025-03-02 18:33:33 -07:00
2025-03-09 12:16:51 -06:00
fn make_remotes<'a> (&self, repo: &'a Repository, cfg: &ConfigFile) -> Result<Vec<String>, String> {
2025-03-05 20:45:35 -07:00
// 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]);
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
2,
format!("Attempting to create remote {} for url {}", remote_id, to));
2025-03-05 20:45:35 -07:00
match repo.remote(remote_id.as_str(), to) {
Ok(_) => remote_list.push(remote_id),
Err(e) => {
if e.code() == ErrorCode::Exists {
common::warning(format!("remote {} already exists, skipping", remote_id));
2025-03-05 20:45:35 -07:00
remote_list.push(remote_id)
} else {
2025-03-09 12:16:51 -06:00
freak_out!(format!("failed to create remote: {}", e));
2025-03-05 20:45:35 -07:00
}
}
}
}
2025-03-05 20:45:35 -07:00
2025-03-09 12:16:51 -06:00
Ok(remote_list)
}
fn push_remotes(&self, cfg: &Config, repo: &Repository, remote_list: &Vec<String>) -> Result<(), String> {
2025-03-05 20:45:35 -07:00
for id in remote_list {
let mut remote = repo.find_remote(&id).unwrap();
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
1,
format!("Pushing to remote: {}", remote.url().unwrap()));
2025-03-05 20:45:35 -07:00
let mut callbacks = RemoteCallbacks::new();
2025-03-09 12:16:51 -06:00
callbacks.credentials(|_,_,_| Cred::ssh_key(
"git",
None,
&Path::new(&cfg.git.ssh_identity_file),
None));
2025-03-06 20:35:40 -07:00
callbacks.certificate_check(|cert, url| {
let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string()));
}
2025-03-09 12:16:51 -06:00
common::warning(
format!("implicitly trusting unknown host {} with sha256 host key {}",
url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec())));
common::warning(
format!("to ignore this error in the future, add this host to your known_hosts file"));
2025-03-06 20:35:40 -07:00
Ok(CertificateCheckStatus::CertificateOk)
});
2025-03-05 20:45:35 -07:00
let mut push_options = PushOptions::new();
push_options.remote_callbacks(callbacks);
2025-03-05 20:45:35 -07:00
let mut refs = Vec::new();
2025-03-08 20:48:28 -07:00
let strings = self.get_refs(&cfg.branches);
2025-03-05 20:45:35 -07:00
for branch in &strings {
refs.push(branch.as_str());
}
2025-03-05 20:45:35 -07:00
match remote.push::<&str>(&refs, Some(&mut push_options)) {
Ok(_) => (),
Err(e) => {
if self.strict {
return Err(format!("failed to push to remote: {}: {}", remote.url().unwrap(), e))
} else {
common::warning(format!("failed to push to remote: {}: {}", remote.url().unwrap(), e))
}
2025-03-05 20:45:35 -07:00
}
}
}
Ok(())
}
fn looper(&self, repos: Vec<OpenedRepository>) -> Result<(), String> {
2025-03-05 20:45:35 -07:00
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(self.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();
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
2,
format!("Sleeping for {} seconds", sleep_int.as_secs()));
2025-03-05 20:45:35 -07:00
while running.load(Ordering::SeqCst) {
thread::sleep(time::Duration::from_secs(1));
if now.elapsed().as_secs() >= sleep_int.as_secs() {
common::verbose(self.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();
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
2,
format!("Interval for {} has arrived, pulling", repos[i].cfg.from));
2025-03-05 20:45:35 -07:00
let _ = self.fast_forward(&repos[i].path, &repos[i].cfg.branches);
if let Err(e) = self.push_remotes(
&repos[i].cfg,
&repos[i].repo,
&repos[i].remotes) {
return Err(e)
}
2025-03-05 20:45:35 -07:00
}
}
2025-03-05 20:45:35 -07:00
do_break = false;
break
}
}
}
common::verbose(self.verbose, 1, format!("Exited looper due to ^C"));
Ok(())
2025-03-05 20:45:35 -07:00
}
2025-03-09 12:16:51 -06:00
pub fn run(&self, cfgs: Vec<ConfigFile>) -> Result<(), String> {
2025-03-05 20:45:35 -07:00
common::verbose(self.verbose, 3, format!("Starting main refractr loop"));
let mut loop_repos = Vec::new();
2025-03-05 20:45:35 -07:00
for cfg in cfgs {
// set up the working directory
2025-03-09 12:16:51 -06:00
common::verbose(self.verbose, 3, format!("Loading config: {}", cfg.path));
let work_dir = self.set_up_work_dir(match &cfg.config.work_dir {
2025-03-05 20:45:35 -07:00
None => {
if cfg!(windows) {
2025-03-15 21:06:02 -06:00
PathBuf::from(format!("{}\\refractr\"", env::var("TEMP").unwrap()))
2025-03-05 20:45:35 -07:00
} else {
PathBuf::from("/tmp/refractr")
}
},
Some(path) => PathBuf::from(path)
});
2025-03-09 12:16:51 -06:00
let path_str = match work_dir {
Ok(p) => p,
Err(e) => return Err(e)
};
common::verbose(
self.verbose,
2,
format!("Created working directory: {}", &path_str));
2025-03-05 20:45:35 -07:00
let repo_name = match &cfg.config.from.split("/").last() {
Some(split) => split.to_string(),
2025-03-09 12:16:51 -06:00
None => freak_out!(format!("failed to parse repository name"))
2025-03-05 20:45:35 -07:00
};
2025-03-02 18:33:33 -07:00
2025-03-05 20:45:35 -07:00
// make initial clone
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
1,
format!("Cloning repository: {}", &cfg.config.from));
2025-03-10 20:56:47 -06:00
let mut builder = RepoBuilder::new();
let mut cb = RemoteCallbacks::new();
2025-03-10 20:56:47 -06:00
let mut fo = FetchOptions::new();
if cfg.config.from.starts_with("ssh://") {
let key_string = cfg.config.git.ssh_identity_file.clone();
cb.credentials(move |_,_,_| Cred::ssh_key(
"git",
None,
Path::new(&key_string),
None));
cb.certificate_check(|cert, url| {
let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string()));
}
common::warning(
format!("implicitly trusting unknown host {} with sha256 host key {}",
url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec())));
common::warning(
format!("to ignore this error in the future, add this host to your known_hosts file"));
Ok(CertificateCheckStatus::CertificateOk)
});
}
2025-03-10 20:56:47 -06:00
fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb);
builder.fetch_options(fo);
2025-03-15 21:06:02 -06:00
let repo_dir = format!("{}{}{}", &path_str, MAIN_SEPARATOR_STR, repo_name);
2025-03-10 20:56:47 -06:00
let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) {
2025-03-05 20:45:35 -07:00
Ok(repo) => repo,
Err(_) => {
common::warning(format!("found existing repo at {}, attempting to use", repo_dir));
2025-03-05 20:45:35 -07:00
match self.fast_forward(&repo_dir, &cfg.config.branches) {
Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) {
repo
} else {
2025-03-09 12:16:51 -06:00
freak_out!(format!("failed to obtain existing repo"))
2025-03-05 20:45:35 -07:00
},
2025-03-09 12:16:51 -06:00
Err(e) => freak_out!(format!("failed to obtain existing repo: {}", e))
2025-03-05 20:45:35 -07:00
}
2025-03-03 21:10:31 -07:00
}
2025-03-05 20:45:35 -07:00
};
2025-03-15 21:06:02 -06:00
self.set_up_refs(&repo, &cfg.config.branches).unwrap();
self.fetch(&repo, &cfg.config.branches, Some(&cfg.config.git.ssh_identity_file)).unwrap();
2025-03-10 20:56:47 -06:00
let remotes = match self.make_remotes(&repo, &cfg) {
2025-03-09 12:16:51 -06:00
Ok(v) => v,
Err(e) => return Err(e)
};
if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) {
return Err(e)
}
2025-03-10 20:56:47 -06:00
2025-03-05 20:45:35 -07:00
if cfg.config.schedule.enabled {
loop_repos.push(OpenedRepository {
repo,
path: repo_dir,
remotes,
cfg: cfg.config
});
2025-03-03 21:10:31 -07:00
}
2025-03-05 20:45:35 -07:00
}
2025-03-03 21:10:31 -07:00
2025-03-05 20:45:35 -07:00
if loop_repos.len() >= 1 {
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
2,
format!("{} configs have schedules enabled, setting up looper", loop_repos.len()));
return self.looper(loop_repos);
2025-03-05 20:45:35 -07:00
} else {
2025-03-09 12:16:51 -06:00
common::verbose(
self.verbose,
2,
format!("No scheduled configs found, exiting refractr"));
2025-03-02 18:33:33 -07:00
}
2025-03-03 22:54:58 -07:00
2025-03-05 20:45:35 -07:00
Ok(())
2025-03-02 18:33:33 -07:00
}
}