fix formatting, use git2 fork until/if merged in

This commit is contained in:
Bryson Steck 2025-03-20 22:01:50 -06:00
parent 5d06e08f5b
commit f37549748c
Signed by: bryson
SSH key fingerprint: SHA256:XpKABw/nP4z8UVaH+weLaBnEOD86+cVwif+QjuYLGT4
7 changed files with 299 additions and 158 deletions

View file

@ -1,4 +1,4 @@
edition = "2021" edition = "2021"
tab_spaces = 2 tab_spaces = 2
match_block_trailing_comma = true match_block_trailing_comma = true
wrap_comments = true #wrap_comments = true

10
Cargo.lock generated
View file

@ -232,9 +232,8 @@ dependencies = [
[[package]] [[package]]
name = "git2" name = "git2"
version = "0.20.0" version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/brysonsteck/git2-rs?branch=certificates#659e318aedf3de9c7be5ce2bf40c8aa850194792"
checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"libc", "libc",
@ -435,9 +434,8 @@ checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
version = "0.18.0+1.9.0" version = "0.18.1+1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/brysonsteck/git2-rs?branch=certificates#659e318aedf3de9c7be5ce2bf40c8aa850194792"
checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",

View file

@ -8,7 +8,7 @@ edition = "2021"
clap = { version = "4.5.29", features = ["derive"] } clap = { version = "4.5.29", features = ["derive"] }
colored = "3.0.0" colored = "3.0.0"
ctrlc = "3.4.5" ctrlc = "3.4.5"
git2 = "0.20.0" git2 = { git = "https://github.com/brysonsteck/git2-rs", branch = "certificates" }
hex = "0.4.3" hex = "0.4.3"
quit = "2.0.0" quit = "2.0.0"
serde = "1.0.217" serde = "1.0.217"

View file

@ -15,7 +15,7 @@ pub enum ExitCode {
RemoteError = 5, RemoteError = 5,
PushError = 6, PushError = 6,
FetchError = 7, FetchError = 7,
ConfigError = 8 ConfigError = 8,
} }
pub struct ReturnData { pub struct ReturnData {
@ -33,7 +33,9 @@ pub fn warning(msg: String) {
} }
pub fn verbose(level: u8, msg_lvl: u8, msg: String) { pub fn verbose(level: u8, msg_lvl: u8, msg: String) {
if level < msg_lvl { return }; if level < msg_lvl {
return;
};
let mut prefix = String::new(); let mut prefix = String::new();
for _ in 0..msg_lvl { for _ in 0..msg_lvl {
prefix += "="; prefix += "=";

View file

@ -20,7 +20,7 @@ use toml;
pub struct ConfigFile { pub struct ConfigFile {
pub path: String, pub path: String,
pub file: Metadata, pub file: Metadata,
pub config: Config pub config: Config,
} }
impl fmt::Display for ConfigFile { impl fmt::Display for ConfigFile {
@ -40,21 +40,24 @@ impl fmt::Display for ConfigFile {
if i < self.config.to.len() - 1 { if i < self.config.to.len() - 1 {
to_list.push_str(", "); to_list.push_str(", ");
} }
}; }
to_list.push(']'); to_list.push(']');
let work_dir_path = match &self.config.work_dir { let work_dir_path = match &self.config.work_dir {
None => { None => {
if cfg!(windows) { if cfg!(windows) {
format!("Using default \"{}\\refractr\"", match env::var("TEMP") { format!(
Ok(val) => val, "Using default \"{}\\refractr\"",
Err(_) => format!("This shouldn't happen!") match env::var("TEMP") {
}) Ok(val) => val,
Err(_) => format!("This shouldn't happen!"),
}
)
} else { } else {
format!("Using default: /tmp/refractr") format!("Using default: /tmp/refractr")
} }
}, },
Some(path) => format!("{}", path) Some(path) => format!("{}", path),
}; };
let schedule_interval = match self.config.schedule.interval { let schedule_interval = match self.config.schedule.interval {
@ -65,10 +68,15 @@ impl fmt::Display for ConfigFile {
String::from("This shouldn't happen!\n") String::from("This shouldn't happen!\n")
} }
}, },
Some(int) => format!("\n Scheduled interval in seconds: {}", int.to_string()) Some(int) => format!(
"\n Scheduled interval in seconds: {}",
int.to_string()
),
}; };
write!(f, "Config file: \"{}\"\n \ write!(
f,
"Config file: \"{}\"\n \
Is a file: {}\n \ Is a file: {}\n \
Read only: {}\n \ Read only: {}\n \
Configuration:\n \ Configuration:\n \
@ -78,32 +86,37 @@ impl fmt::Display for ConfigFile {
Working directory: {}\n \ Working directory: {}\n \
SSH key for pushing clone: {}\n \ SSH key for pushing clone: {}\n \
Schedule enabled: {}\ Schedule enabled: {}\
{}" {}",
, self.path, self.file.is_file(), self.file.permissions().readonly(), self.config.from self.path,
, to_list, branches_list, work_dir_path, self.config.git.ssh_identity_file, self.config.schedule.enabled self.file.is_file(),
, schedule_interval) self.file.permissions().readonly(),
self.config.from,
to_list,
branches_list,
work_dir_path,
self.config.git.ssh_identity_file,
self.config.schedule.enabled,
schedule_interval
)
} }
} }
#[derive(PartialEq)] #[derive(PartialEq, Deserialize)]
#[derive(Deserialize)]
pub struct Config { pub struct Config {
pub from: String, pub from: String,
pub to: Vec<String>, pub to: Vec<String>,
pub branches: Vec<String>, pub branches: Vec<String>,
pub work_dir: Option<String>, pub work_dir: Option<String>,
pub git: Git, pub git: Git,
pub schedule: Schedule pub schedule: Schedule,
} }
#[derive(PartialEq)] #[derive(PartialEq, Deserialize)]
#[derive(Deserialize)]
pub struct Git { pub struct Git {
pub ssh_identity_file: String, pub ssh_identity_file: String,
} }
#[derive(PartialEq)] #[derive(PartialEq, Deserialize)]
#[derive(Deserialize)]
pub struct Schedule { pub struct Schedule {
pub enabled: bool, pub enabled: bool,
pub interval: Option<i32>, pub interval: Option<i32>,
@ -112,48 +125,93 @@ pub struct Schedule {
pub fn read_config(paths: Vec<PathBuf>, refractr: &Refractr) -> Result<Vec<ConfigFile>, String> { pub fn read_config(paths: Vec<PathBuf>, refractr: &Refractr) -> Result<Vec<ConfigFile>, String> {
let mut config_files: Vec<ConfigFile> = vec![]; let mut config_files: Vec<ConfigFile> = vec![];
for path in paths { for path in paths {
common::verbose(refractr.verbose, 1, format!("Reading config file: \"{}\"", String::from(path.to_string_lossy()))); common::verbose(
refractr.verbose,
1,
format!(
"Reading config file: \"{}\"",
String::from(path.to_string_lossy())
),
);
let mut data = String::new(); let mut data = String::new();
let mut file = match File::open(path.as_path()) { let mut file = match File::open(path.as_path()) {
Err(e) => return Err(format!("unable to open {}: {}", path.as_path().display(), e)), Err(e) => {
Ok(file) => file return Err(format!(
"unable to open {}: {}",
path.as_path().display(),
e
))
},
Ok(file) => file,
}; };
if let Err(e) = file.read_to_string(&mut data) { if let Err(e) = file.read_to_string(&mut data) {
return Err(format!("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) { let config: Config = match toml::from_str(&data) {
Ok(c) => c, Ok(c) => c,
Err(e) => return Err(format!("issues parsing toml file {}: {}", path.as_path().display(), e)) Err(e) => {
return Err(format!(
"issues parsing toml file {}: {}",
path.as_path().display(),
e
))
},
}; };
let config_file = ConfigFile { let config_file = ConfigFile {
path: match fs::canonicalize(&path) { path: match fs::canonicalize(&path) {
Err(_) => return Err(format!("cannot get absolute path of config file: {}", path.as_path().display())), Err(_) => {
Ok(abs) => abs.to_string_lossy().to_string() 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) { file: match fs::metadata(&path) {
Err(_) => return Err(format!("cannot obtain metadata for config file: {}", path.as_path().display())), Err(_) => {
Ok(metadata) => metadata return Err(format!(
"cannot obtain metadata for config file: {}",
path.as_path().display()
))
},
Ok(metadata) => metadata,
}, },
config: match verify_config(&config) { config: match verify_config(&config) {
Err(e) => return Err(format!("invalid config {}: {}", path.as_path().display(), e)), Err(e) => {
Ok(_) => config return Err(format!(
} "invalid config {}: {}",
path.as_path().display(),
e
))
},
Ok(_) => config,
},
}; };
let mut dup = false; let mut dup = false;
for i in &config_files { for i in &config_files {
if i.path == config_file.path { if i.path == config_file.path {
common::warning(format!("skipping config file \"{}\" as it was already read", path.as_path().display())); common::warning(format!(
"skipping config file \"{}\" as it was already read",
path.as_path().display()
));
dup = true; dup = true;
break; break;
} else if i.config == config_file.config { } else if i.config == config_file.config {
common::warning(format!("config files \"{}\" and \"{}\" appear to have the same config", i.path, config_file.path)); common::warning(format!(
"config files \"{}\" and \"{}\" appear to have the same config",
i.path, config_file.path
));
} }
} }
if !dup { if !dup {
config_files.push(config_file); config_files.push(config_file);
} }
@ -167,10 +225,10 @@ fn verify_config(config: &Config) -> Result<(), String> {
match config.schedule.interval { match config.schedule.interval {
Some(i) => { Some(i) => {
if i < 60 { if i < 60 {
return Err(format!("schedule is enabled, but less than 60")) return Err(format!("schedule is enabled, but less than 60"));
} }
}, },
None => return Err(format!("schedule is enabled, but no interval was defined")) None => return Err(format!("schedule is enabled, but no interval was defined")),
} }
} }
@ -180,7 +238,7 @@ fn verify_config(config: &Config) -> Result<(), String> {
if cfg!(windows) { if cfg!(windows) {
match env::var("TEMP") { match env::var("TEMP") {
Ok(val) => val, Ok(val) => val,
Err(_) => return Err(format!("cannot determine the default temp dir")) Err(_) => return Err(format!("cannot determine the default temp dir")),
} }
} else { } else {
format!("/tmp/refractr") format!("/tmp/refractr")
@ -190,8 +248,9 @@ fn verify_config(config: &Config) -> Result<(), String> {
if !&config.from.starts_with("ssh://") if !&config.from.starts_with("ssh://")
&& !&config.from.starts_with("https://") && !&config.from.starts_with("https://")
&& !&config.from.starts_with("http://") { && !&config.from.starts_with("http://")
return Err(format!("'from' value does not use a supported protocol")) {
return Err(format!("'from' value does not use a supported protocol"));
} }
Ok(()) Ok(())

View file

@ -15,10 +15,10 @@ use crate::refractr::Refractr;
use clap::Parser; use clap::Parser;
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
#[cfg(target_family = "unix")]
use users;
#[cfg(target_family = "windows")] #[cfg(target_family = "windows")]
use username; use username;
#[cfg(target_family = "unix")]
use users;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "refractr")] #[command(name = "refractr")]
@ -32,7 +32,11 @@ struct Args {
#[arg(short, long, help = "Specify the level of verbosity", action = clap::ArgAction::Count)] #[arg(short, long, help = "Specify the level of verbosity", action = clap::ArgAction::Count)]
verbose: u8, verbose: u8,
#[arg(short = 'e', long, help = "Output a full, commented config file and exit")] #[arg(
short = 'e',
long,
help = "Output a full, commented config file and exit"
)]
create: bool, create: bool,
#[arg(short = 's', long, help = "Exit on push errors instead of ignoring")] #[arg(short = 's', long, help = "Exit on push errors instead of ignoring")]
@ -44,7 +48,8 @@ fn get_config_default() -> &'static str {
return "C:\\ProgramData\\refractr\\config.toml"; return "C:\\ProgramData\\refractr\\config.toml";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
return "/etc/refractr/config.toml"; return "/etc/refractr/config.toml";
#[cfg(any(target_os = "freebsd", #[cfg(any(
target_os = "freebsd",
target_os = "openbsd", target_os = "openbsd",
target_os = "netbsd", target_os = "netbsd",
target_os = "macos" target_os = "macos"
@ -61,12 +66,12 @@ fn main() -> Result<(), String> {
"true" => true, "true" => true,
_ => false, _ => false,
}, },
None => false None => false,
}, },
pid: process::id(), pid: process::id(),
strict: args.strict, strict: args.strict,
unix: cfg!(unix), unix: cfg!(unix),
verbose: args.verbose verbose: args.verbose,
}; };
// warn to avoid root/admin // warn to avoid root/admin
@ -86,22 +91,37 @@ fn main() -> Result<(), String> {
Err(_) => common::warning(format!("failed to get process username")), Err(_) => common::warning(format!("failed to get process username")),
} }
common::verbose(refractr.verbose, 1, format!("refractr started with 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!("Process ID: {}", refractr.pid));
common::verbose(refractr.verbose, 3, format!("Running in Docker: {}", refractr.docker)); common::verbose(
common::verbose(refractr.verbose, 3, format!("System is UNIX(-like): {}", refractr.unix)); 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")); common::verbose(refractr.verbose, 2, format!("Checking for create flag"));
if args.create { if args.create {
common::verbose(refractr.verbose, 3, format!("Printing sample config")); common::verbose(refractr.verbose, 3, format!("Printing sample config"));
let example = include_str!("example/config.toml"); let example = include_str!("example/config.toml");
println!("{}", example); println!("{}", example);
return Ok(()) return Ok(());
} }
let mut cfgs = vec![]; let mut cfgs = vec![];
match config::read_config(args.config, &refractr) { match config::read_config(args.config, &refractr) {
Ok(c) => cfgs = c, Ok(c) => cfgs = c,
Err(e) => common::error(format!("{}", e), common::ExitCode::ConfigError) Err(e) => common::error(format!("{}", e), common::ExitCode::ConfigError),
}; };
if refractr.verbose >= 2 { if refractr.verbose >= 2 {
@ -111,10 +131,14 @@ fn main() -> Result<(), String> {
} }
} }
common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully")); common::verbose(
refractr.verbose,
1,
format!("Config file(s) read successfully"),
);
match refractr.run(cfgs) { match refractr.run(cfgs) {
Ok(_) => (), Ok(_) => (),
Err(e) => common::error(format!("{}", e.msg), e.code) Err(e) => common::error(format!("{}", e.msg), e.code),
}; };
Ok(()) Ok(())

View file

@ -6,20 +6,20 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
use crate::common::{self, ReturnData, ExitCode}; use crate::common::{self, ExitCode, ReturnData};
use crate::config::{Config, ConfigFile}; use crate::config::{Config, ConfigFile};
use git2::string_array::StringArray;
use git2::{FetchOptions, CertificateCheckStatus, Cred, PushOptions, RemoteCallbacks, Repository};
use git2::{Error, ErrorCode};
use git2::build::RepoBuilder; use git2::build::RepoBuilder;
use git2::string_array::StringArray;
use git2::{CertificateCheckStatus, Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository};
use git2::{Error, ErrorCode};
use hex; use hex;
use sha2::{Sha256, Digest}; use sha2::{Digest, Sha256};
use std::env; use std::env;
use std::fs; use std::fs;
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread; use std::thread;
use std::time; use std::time;
@ -28,7 +28,7 @@ pub struct Refractr {
pub pid: u32, pub pid: u32,
pub strict: bool, pub strict: bool,
pub unix: bool, pub unix: bool,
pub verbose: u8 pub verbose: u8,
} }
struct OpenedRepository { struct OpenedRepository {
@ -41,7 +41,11 @@ struct OpenedRepository {
impl Refractr { impl Refractr {
fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<String, String> { fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<String, String> {
if let Err(e) = fs::create_dir_all(&work_dir) { if let Err(e) = fs::create_dir_all(&work_dir) {
return Err(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()) Ok(work_dir.to_string_lossy().to_string())
} }
@ -72,47 +76,58 @@ impl Refractr {
Ok(()) Ok(())
} }
fn fetch(&self, repo: &Repository, branches: &Vec<String>, ssh: bool, ssh_key: &String) -> Result<(), Error> { fn fetch(
&self,
repo: &Repository,
branches: &Vec<String>,
ssh: bool,
ssh_key: &String,
) -> Result<(), Error> {
let mut cb = RemoteCallbacks::new(); let mut cb = RemoteCallbacks::new();
let mut fo = FetchOptions::new(); let mut fo = FetchOptions::new();
if ssh { if ssh {
let key_string: String = ssh_key.clone(); let key_string: String = ssh_key.clone();
cb.credentials(move |_,_,_| Cred::ssh_key( cb.credentials(move |_, _, _| Cred::ssh_key("git", None, Path::new(&key_string), None));
"git",
None,
Path::new(&key_string),
None));
cb.certificate_check(|cert, url| { cb.certificate_check(|cert, url| {
let mut sha256 = String::new(); let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() { for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string())); sha256.push_str(&hex::encode(i.to_string()));
} }
common::warning( common::warning(format!(
format!("implicitly trusting unknown host {} with sha256 host key {}", "implicitly trusting unknown host {} with sha256 host key {}",
url, url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec()))); 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")); common::warning(format!(
"to ignore this error in the future, add this host to your known_hosts file"
));
Ok(CertificateCheckStatus::CertificateOk) Ok(CertificateCheckStatus::CertificateOk)
}); });
} }
fo.download_tags(git2::AutotagOption::All); fo.download_tags(git2::AutotagOption::All);
fo.remote_callbacks(cb); fo.remote_callbacks(cb);
repo.find_remote("origin")?.fetch(&branches, Some(&mut fo), None)?; repo
.find_remote("origin")?
.fetch(&branches, Some(&mut fo), None)?;
Ok(()) Ok(())
} }
fn set_up_refs(&self, repo: &Repository, branches: &Vec<String>) -> Result<(), Error> { fn set_up_refs(&self, repo: &Repository, branches: &Vec<String>) -> Result<(), Error> {
for branch in branches { for branch in branches {
let mut fetch_head = repo.find_reference(format!("refs/remotes/origin/{}", branch).as_str())?; let mut fetch_head =
repo.find_reference(format!("refs/remotes/origin/{}", branch).as_str())?;
fetch_head.rename(format!("refs/heads/{}", branch).as_str(), true, "")?; fetch_head.rename(format!("refs/heads/{}", branch).as_str(), true, "")?;
} }
Ok(()) Ok(())
} }
fn make_remotes<'a> (&self, repo: &'a Repository, cfg: &ConfigFile) -> Result<Vec<String>, String> { fn make_remotes<'a>(
&self,
repo: &'a Repository,
cfg: &ConfigFile,
) -> Result<Vec<String>, String> {
// create remotes for each "to" repo // create remotes for each "to" repo
let mut remote_list = Vec::new(); let mut remote_list = Vec::new();
for to in &cfg.config.to { for to in &cfg.config.to {
@ -122,7 +137,8 @@ impl Refractr {
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("Attempting to create remote {} for url {}", remote_id, to)); format!("Attempting to create remote {} for url {}", remote_id, to),
);
match repo.remote(remote_id.as_str(), to) { match repo.remote(remote_id.as_str(), to) {
Ok(_) => remote_list.push(remote_id), Ok(_) => remote_list.push(remote_id),
Err(e) => { Err(e) => {
@ -132,37 +148,43 @@ impl Refractr {
} else { } else {
return Err(format!("failed to create remote: {}", e)); return Err(format!("failed to create remote: {}", e));
} }
} },
} }
} }
Ok(remote_list) Ok(remote_list)
} }
fn push_remotes(&self, cfg: &Config, repo: &Repository, remote_list: &Vec<String>) -> Result<(), String> { fn push_remotes(
&self,
cfg: &Config,
repo: &Repository,
remote_list: &Vec<String>,
) -> Result<(), String> {
for id in remote_list { for id in remote_list {
let mut remote = repo.find_remote(&id).unwrap(); let mut remote = repo.find_remote(&id).unwrap();
common::verbose( common::verbose(
self.verbose, self.verbose,
1, 1,
format!("Pushing to remote: {}", remote.url().unwrap())); format!("Pushing to remote: {}", remote.url().unwrap()),
);
let mut callbacks = RemoteCallbacks::new(); let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_,_,_| Cred::ssh_key( callbacks.credentials(|_, _, _| {
"git", Cred::ssh_key("git", None, &Path::new(&cfg.git.ssh_identity_file), None)
None, });
&Path::new(&cfg.git.ssh_identity_file),
None));
callbacks.certificate_check(|cert, url| { callbacks.certificate_check(|cert, url| {
let mut sha256 = String::new(); let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() { for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string())); sha256.push_str(&hex::encode(i.to_string()));
} }
common::warning( common::warning(format!(
format!("implicitly trusting unknown host {} with sha256 host key {}", "implicitly trusting unknown host {} with sha256 host key {}",
url, url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec()))); 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")); common::warning(format!(
"to ignore this error in the future, add this host to your known_hosts file"
));
Ok(CertificateCheckStatus::CertificateOk) Ok(CertificateCheckStatus::CertificateOk)
}); });
let mut push_options = PushOptions::new(); let mut push_options = PushOptions::new();
@ -182,11 +204,19 @@ impl Refractr {
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
if self.strict { if self.strict {
return Err(format!("failed to push to remote: {}: {}", remote.url().unwrap(), e)) return Err(format!(
"failed to push to remote: {}: {}",
remote.url().unwrap(),
e
));
} else { } else {
common::warning(format!("failed to push to remote: {}: {}", remote.url().unwrap(), e)) common::warning(format!(
"failed to push to remote: {}: {}",
remote.url().unwrap(),
e
))
} }
} },
} }
} }
@ -199,13 +229,16 @@ impl Refractr {
let r = running.clone(); let r = running.clone();
let count = repos.len(); let count = repos.len();
for i in 0..repos.len() { for i in 0..repos.len() {
current_ints.push(u64::from(repos[i].cfg.schedule.interval.unwrap().unsigned_abs())); current_ints.push(u64::from(
}; repos[i].cfg.schedule.interval.unwrap().unsigned_abs(),
));
}
let original_ints = current_ints.clone(); let original_ints = current_ints.clone();
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst); r.store(false, Ordering::SeqCst);
}).expect("Failed to set ^C handler"); })
.expect("Failed to set ^C handler");
common::verbose(self.verbose, 1, format!("Starting scheduled loop")); common::verbose(self.verbose, 1, format!("Starting scheduled loop"));
let min = *current_ints.iter().min().unwrap(); let min = *current_ints.iter().min().unwrap();
@ -218,7 +251,8 @@ impl Refractr {
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("Sleeping for {} seconds", sleep_int.as_secs())); format!("Sleeping for {} seconds", sleep_int.as_secs()),
);
while running.load(Ordering::SeqCst) { while running.load(Ordering::SeqCst) {
thread::sleep(time::Duration::from_secs(1)); thread::sleep(time::Duration::from_secs(1));
if now.elapsed().as_secs() >= sleep_int.as_secs() { if now.elapsed().as_secs() >= sleep_int.as_secs() {
@ -230,19 +264,17 @@ impl Refractr {
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("Interval for {} has arrived, pulling", repos[i].cfg.from)); format!("Interval for {} has arrived, pulling", repos[i].cfg.from),
);
let _ = self.fast_forward(&repos[i].path, &repos[i].cfg.branches); let _ = self.fast_forward(&repos[i].path, &repos[i].cfg.branches);
if let Err(e) = self.push_remotes( if let Err(e) = self.push_remotes(&repos[i].cfg, &repos[i].repo, &repos[i].remotes) {
&repos[i].cfg, common::error(e, ExitCode::PushError)
&repos[i].repo,
&repos[i].remotes) {
common::error(e, ExitCode::PushError)
}; };
} }
} }
do_break = false; do_break = false;
break break;
} }
} }
} }
@ -266,51 +298,54 @@ impl Refractr {
PathBuf::from("/tmp/refractr") PathBuf::from("/tmp/refractr")
} }
}, },
Some(path) => PathBuf::from(path) Some(path) => PathBuf::from(path),
}); });
let path_str = match work_dir { let path_str = match work_dir {
Ok(p) => p, Ok(p) => p,
Err(e) => return Err(ReturnData { Err(e) => {
code: ExitCode::FilesystemError, return Err(ReturnData {
msg: e code: ExitCode::FilesystemError,
}) msg: e,
})
},
}; };
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("Created working directory: {}", &path_str)); format!("Created working directory: {}", &path_str),
);
let repo_name = match &cfg.config.from.split("/").last() { let repo_name = match &cfg.config.from.split("/").last() {
Some(split) => split.to_string(), Some(split) => split.to_string(),
None => return Err(ReturnData { None => {
code: ExitCode::ParseError, return Err(ReturnData {
msg: format!("failed to parse repository name") code: ExitCode::ParseError,
}) msg: format!("failed to parse repository name"),
})
},
}; };
let ssh = cfg.config.from.starts_with("ssh://"); let ssh = cfg.config.from.starts_with("ssh://");
let mut builder = RepoBuilder::new(); let mut builder = RepoBuilder::new();
let mut cb = RemoteCallbacks::new(); let mut cb = RemoteCallbacks::new();
let mut fo = FetchOptions::new(); let mut fo = FetchOptions::new();
// make initial clone // make initial clone
if ssh { if ssh {
let key_string = cfg.config.git.ssh_identity_file.clone(); let key_string = cfg.config.git.ssh_identity_file.clone();
cb.credentials(move |_,_,_| Cred::ssh_key( cb.credentials(move |_, _, _| Cred::ssh_key("git", None, Path::new(&key_string), None));
"git",
None,
Path::new(&key_string),
None));
cb.certificate_check(|cert, url| { cb.certificate_check(|cert, url| {
let mut sha256 = String::new(); let mut sha256 = String::new();
for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() { for i in cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec() {
sha256.push_str(&hex::encode(i.to_string())); sha256.push_str(&hex::encode(i.to_string()));
} }
common::warning( common::warning(format!(
format!("implicitly trusting unknown host {} with sha256 host key {}", "implicitly trusting unknown host {} with sha256 host key {}",
url, url,
hex::encode(cert.as_hostkey().unwrap().hash_sha256().unwrap().to_vec()))); 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")); common::warning(format!(
"to ignore this error in the future, add this host to your known_hosts file"
));
Ok(CertificateCheckStatus::CertificateOk) Ok(CertificateCheckStatus::CertificateOk)
}); });
} }
@ -322,45 +357,63 @@ impl Refractr {
common::verbose( common::verbose(
self.verbose, self.verbose,
1, 1,
format!("Cloning repository {} to {}", &cfg.config.from, &repo_dir)); format!("Cloning repository {} to {}", &cfg.config.from, &repo_dir),
);
let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) { let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) {
Ok(repo) => repo, Ok(repo) => repo,
Err(e) => { Err(e) => {
if e.code() != ErrorCode::Exists { if e.code() != ErrorCode::Exists {
common::error(format!("failed to clone repo to {}: {}", repo_dir, e), ExitCode::FilesystemError); common::error(
format!("failed to clone repo to {}: {}", repo_dir, e),
ExitCode::FilesystemError,
);
} }
common::warning(format!("found existing repo at {}, attempting to use", repo_dir)); common::warning(format!(
"found existing repo at {}, attempting to use",
repo_dir
));
match self.fast_forward(&repo_dir, &cfg.config.branches) { match self.fast_forward(&repo_dir, &cfg.config.branches) {
Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) { Ok(_) => {
repo if let Ok(repo) = Repository::open(Path::new(&repo_dir)) {
} else { repo
} else {
return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!("failed to obtain existing repo"),
});
}
},
Err(e) => {
return Err(ReturnData { return Err(ReturnData {
code: ExitCode::RepositoryError, code: ExitCode::RepositoryError,
msg: format!("failed to obtain existing repo") msg: format!("failed to obtain existing repo: {}", e),
}) })
}, },
Err(e) => return Err(ReturnData {
code: ExitCode::RepositoryError,
msg: format!("failed to obtain existing repo: {}", e)
})
} }
} },
}; };
self.set_up_refs(&repo, &cfg.config.branches).unwrap(); self.set_up_refs(&repo, &cfg.config.branches).unwrap();
if let Err(e) = self.fetch(&repo, if let Err(e) = self.fetch(
&repo,
&cfg.config.branches, &cfg.config.branches,
ssh, ssh,
&cfg.config.git.ssh_identity_file) { &cfg.config.git.ssh_identity_file,
common::error(format!("failed to fetch repo {}: {}", cfg.config.from, e), ExitCode::FetchError); ) {
common::error(
format!("failed to fetch repo {}: {}", cfg.config.from, e),
ExitCode::FetchError,
);
} }
let remotes = match self.make_remotes(&repo, &cfg) { let remotes = match self.make_remotes(&repo, &cfg) {
Ok(v) => v, Ok(v) => v,
Err(e) => return Err(ReturnData { Err(e) => {
code: ExitCode::RemoteError, return Err(ReturnData {
msg: e code: ExitCode::RemoteError,
}) msg: e,
})
},
}; };
if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) { if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) {
common::error(e, ExitCode::PushError); common::error(e, ExitCode::PushError);
@ -371,7 +424,7 @@ impl Refractr {
repo, repo,
path: repo_dir, path: repo_dir,
remotes, remotes,
cfg: cfg.config cfg: cfg.config,
}); });
} }
} }
@ -380,13 +433,18 @@ impl Refractr {
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("{} configs have schedules enabled, setting up looper", loop_repos.len())); format!(
"{} configs have schedules enabled, setting up looper",
loop_repos.len()
),
);
return self.looper(loop_repos); return self.looper(loop_repos);
} else { } else {
common::verbose( common::verbose(
self.verbose, self.verbose,
2, 2,
format!("No scheduled configs found, exiting refractr")); format!("No scheduled configs found, exiting refractr"),
);
} }
Ok(()) Ok(())