From 4bff7455738c00303286e89b4d2b1c2f37a8c3a3 Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Sat, 8 Jan 2022 15:34:09 -0500 Subject: [PATCH] Goofy first approach with application. Separate into copy module. --- src/copy.rs | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/daemon.rs | 7 +- src/git.rs | 84 +---------------------- src/lib.rs | 12 ++-- src/main.rs | 20 ++++++ 5 files changed, 211 insertions(+), 91 deletions(-) create mode 100644 src/copy.rs diff --git a/src/copy.rs b/src/copy.rs new file mode 100644 index 0000000..afcb115 --- /dev/null +++ b/src/copy.rs @@ -0,0 +1,179 @@ +use super::{config::PathConfig, path, path::ResPathBuf}; +use simplelog::{info, paris}; +use std::{ + collections::HashMap, + env::VarError, + error, fmt, fs, io, + path::{Path, PathBuf}, + result, +}; + +// ======================================== +// Error +// ======================================== + +pub type Result = result::Result; + +#[derive(Debug)] +pub enum Error { + IOError(io::Error), + VarError(VarError), +} + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::IOError(err) + } +} + +impl From for Error { + fn from(err: VarError) -> Error { + Error::VarError(err) + } +} + +impl From for Error { + fn from(err: path::Error) -> Error { + match err { + path::Error::IOError(e) => Error::IOError(e), + path::Error::VarError(e) => Error::VarError(e), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::IOError(e) => write!(f, "{}", e), + Error::VarError(e) => write!(f, "{}", e), + } + } +} + +impl error::Error for Error {} + +// ======================================== +// Application +// ======================================== + +fn apply_all(pc: &PathConfig) -> Result<()> { + let workdir = path::resolve(&pc.config.repos.local)?; + let repo_lookup = get_repo_lookup(workdir.as_ref(), workdir.as_ref())?; + let package_lookup = get_package_lookup(pc); + + for (repo_unresolved, repo_resolved) in &repo_lookup { + if let Some(package_resolved) = package_lookup.get(repo_unresolved) { + fs::copy(repo_resolved, package_resolved)?; + info!( + "Copied `{}` from local repository.", + repo_unresolved.display(), + ); + } + } + + Ok(()) +} + +fn apply_one(pc: &PathConfig, target: &Path) -> Result<()> { + let workdir = path::resolve(&pc.config.repos.local)?; + let repo_lookup = get_repo_lookup(workdir.as_ref(), workdir.as_ref())?; + let package_lookup = get_package_lookup(pc); + + // The user must specify a path that matches the unresolved one. + if let Some(repo_resolved) = repo_lookup.get(target) { + if let Some(package_resolved) = package_lookup.get(target) { + fs::copy(repo_resolved, package_resolved)?; + info!("Copied `{}` from local repository.", target.display(),); + } + } + + Ok(()) +} + +pub fn apply(pc: &PathConfig, file: Option<&str>) -> Result<()> { + if let Some(file) = file { + apply_one(pc, Path::new(file)) + } else { + apply_all(pc) + } +} + +// ======================================== +// Staging +// ======================================== + +pub fn stage(pc: &PathConfig) -> Result<()> { + let workdir = path::resolve(&pc.config.repos.local)?; + let repo_lookup = get_repo_lookup(workdir.as_ref(), workdir.as_ref())?; + let package_lookup = get_package_lookup(pc); + + // Find all files in our repository that are no longer being referenced in + // our primary config file. They should be removed from the repository. + for (repo_unresolved, repo_resolved) in &repo_lookup { + if !package_lookup.contains_key(repo_unresolved) { + fs::remove_file(repo_resolved)?; + } + if let Some(p) = repo_resolved.resolved().parent() { + if p.read_dir()?.next().is_none() { + fs::remove_dir(p)?; + } + } + } + + // Find all resolvable files in our primary config and copy them into the + // repository. + for (package_unresolved, package_resolved) in &package_lookup { + let mut copy = package_resolved.resolved().to_path_buf(); + copy.push(package_unresolved); + if let Some(p) = copy.parent() { + fs::create_dir_all(p)?; + } + fs::copy(package_resolved.resolved(), copy)?; + } + + info!( + "Staged files. Run git -C {} status to see what changed.", + &pc.config.repos.local.display() + ); + + Ok(()) +} + +// ======================================== +// Utility +// ======================================== + +fn get_repo_lookup(root: &Path, path: &Path) -> Result> { + let mut seen = HashMap::new(); + if path.is_dir() { + for entry in fs::read_dir(path)? { + let nested = entry?.path(); + if nested.is_dir() { + if nested.ends_with(".git") { + continue; + } + let nested = get_repo_lookup(root, &nested)?; + seen.extend(nested); + } else { + let relative = nested + .strip_prefix(root) + .expect("Relative git file could not be stripped properly.") + .to_path_buf(); + seen.insert(relative, ResPathBuf::new(&nested)?); + } + } + } + Ok(seen) +} + +fn get_package_lookup(pc: &PathConfig) -> HashMap { + let mut seen = HashMap::new(); + for (_, packages) in &pc.config.packages { + for path in packages { + if let Ok(resolved) = path::resolve(path) { + seen.insert(path.to_path_buf(), resolved); + } + } + } + seen +} diff --git a/src/daemon.rs b/src/daemon.rs index e4d2941..d3977dc 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,5 +1,4 @@ -use super::{config, config::PathConfig, git, path, path::ResPathBuf}; -use git2::Repository; +use super::{config, config::PathConfig, copy, path, path::ResPathBuf}; use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; use simplelog::{error, paris, trace, warn}; use std::{ @@ -139,7 +138,7 @@ impl<'a> WatchState<'a> { // Daemon // ======================================== -pub fn launch(mut pc: PathConfig, repo: Repository, freq_secs: u64) -> Result<(), Box> { +pub fn launch(mut pc: PathConfig, freq_secs: u64) -> Result<(), Box> { let (poll_tx, poll_rx) = channel(); let (watch_tx, watch_rx) = channel(); let watch_tx1 = watch_tx.clone(); @@ -157,7 +156,7 @@ pub fn launch(mut pc: PathConfig, repo: Repository, freq_secs: u64) -> Result<() let mut state = WatchState::new(poll_tx, &mut watcher)?; state.update(&pc); loop { - git::stage(&pc, &repo)?; + copy::stage(&pc)?; // Received paths should always be fully resolved. match watch_rx.recv() { Ok(DebouncedEvent::NoticeWrite(p)) => { diff --git a/src/git.rs b/src/git.rs index 4339a37..f4d3494 100644 --- a/src/git.rs +++ b/src/git.rs @@ -4,12 +4,11 @@ use git2::{ ObjectType, PushOptions, Remote, RemoteCallbacks, Repository, Signature, StashApplyOptions, StashFlags, }; -use path::ResPathBuf; use simplelog::{info, paris, warn}; use std::{ collections::HashSet, env::VarError, - error, fmt, fs, io, + error, fmt, io, path::{Path, PathBuf}, result, }; @@ -135,87 +134,6 @@ pub fn init(pc: &PathConfig) -> Result { } } -// ======================================== -// Staging -// ======================================== - -fn find_repo_files(path: &Path) -> Result> { - let mut seen = Vec::new(); - if path.is_dir() { - for entry in fs::read_dir(path)? { - let nested = entry?.path(); - if nested.is_dir() { - if nested.ends_with(".git") { - continue; - } - let nested = find_repo_files(&nested)?; - seen.extend_from_slice(&nested); - } else if !nested.ends_with(".homesync") { - seen.push(ResPathBuf::new(&nested)?); - } - } - } - Ok(seen) -} - -fn find_package_files(pc: &PathConfig) -> Vec { - let mut seen = Vec::new(); - for (_, packages) in &pc.config.packages { - for path in packages { - if let Ok(resolved) = path::resolve(path) { - seen.push(resolved); - } - } - } - seen -} - -pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> { - let workdir = check_working_repo(repo)?; - let repo_files = find_repo_files(&workdir)?; - let package_files = find_package_files(pc); - - // Find all files in our repository that are no longer being referenced in - // our primary config file. They should be removed from the repository. - let lookup_files: HashSet = package_files - .iter() - .map(|m| m.unresolved().to_path_buf()) - .collect(); - for repo_file in &repo_files { - let relative = repo_file - .resolved() - .strip_prefix(&workdir) - .expect("Relative git file could not be stripped properly.") - .to_path_buf(); - if !lookup_files.contains(&relative) { - fs::remove_file(repo_file)?; - } - if let Some(p) = repo_file.resolved().parent() { - if p.read_dir()?.next().is_none() { - fs::remove_dir(p)?; - } - } - } - - // Find all resolvable files in our primary config and copy them into the - // repository. - for package_file in &package_files { - let mut copy = workdir.to_path_buf(); - copy.push(package_file.unresolved()); - if let Some(p) = copy.parent() { - fs::create_dir_all(p)?; - } - fs::copy(package_file.resolved(), copy)?; - } - - info!( - "Staged files. Run git -C {} status to see what changed.", - &pc.config.repos.local.display() - ); - - Ok(()) -} - // ======================================== // Syncing // ======================================== diff --git a/src/lib.rs b/src/lib.rs index e64efec..deeed14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod copy; pub mod daemon; pub mod git; pub mod path; @@ -8,9 +9,13 @@ use std::error::Error; type Result = std::result::Result<(), Box>; +pub fn run_apply(config: PathConfig, file: Option<&str>) -> Result { + copy::apply(&config, file)?; + Ok(()) +} + pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result { - let repo = git::init(&config)?; - daemon::launch(config, repo, freq_secs)?; + daemon::launch(config, freq_secs)?; Ok(()) } @@ -32,7 +37,6 @@ pub fn run_pull(config: PathConfig) -> Result { } pub fn run_stage(config: PathConfig) -> Result { - let repo = git::init(&config)?; - git::stage(&config, &repo)?; + copy::stage(&config)?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 35b84f0..1e88baf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,25 @@ fn main() { .help("Specify a configuration file to use in place of defaults") .takes_value(true), ) + .subcommand( + App::new("apply") + .about("Copy files from local repository to rest of desktop") + .arg( + Arg::new("file") + .value_name("FILE") + .conflicts_with("all") + .required_unless_present("all") + .help("The file we want to overwrite from the local repository") + .takes_value(true), + ) + .arg( + Arg::new("all") + .long("all") + .conflicts_with("file") + .help("Indicates we want to copy all files from the local repository") + .takes_value(false), + ), + ) .subcommand( App::new("daemon") .about("Start up a new homesync daemon") @@ -74,6 +93,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box> { let candidates = find_candidates(&matches)?; let config = homesync::config::load(&candidates)?; match matches.subcommand() { + Some(("apply", matches)) => Ok(homesync::run_apply(config, matches.value_of("file"))?), Some(("daemon", matches)) => { let freq_secs: u64 = match matches.value_of("frequency") { Some(f) => f.parse().unwrap_or(0),