diff --git a/src/cli.rs b/src/cli.rs index 1986100..5847912 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,14 @@ use super::config::PathConfig; -use super::path::ResPathBuf; -use super::{config, path}; +use super::{config, git, path}; use ansi_term::Colour::{Green, Yellow}; use std::env::VarError; use std::io::Write; use std::path::PathBuf; -use std::{error, fmt, fs, io}; +use std::{error, fmt, io}; use url::{ParseError, Url}; +// TODO(jrpotter): Use curses to make this module behave nicer. + // ======================================== // Error // ======================================== @@ -28,6 +29,15 @@ impl From for Error { } } +impl From for Error { + fn from(err: git::Error) -> Error { + match err { + git::Error::IOError(e) => Error::IOError(e), + git::Error::VarError(e) => Error::VarError(e), + } + } +} + impl From for Error { fn from(err: io::Error) -> Error { Error::IOError(err) @@ -72,9 +82,7 @@ impl error::Error for Error {} // Prompts // ======================================== -// TODO(jrpotter): Use curses to make this module behave nicer. - -fn prompt_local(config: &PathConfig) -> Result { +fn prompt_local(config: &PathConfig) -> Result { print!( "Local git repository <{}> (enter to continue): ", Yellow.paint( @@ -86,16 +94,9 @@ fn prompt_local(config: &PathConfig) -> Result { ) ); io::stdout().flush()?; - let mut local = String::new(); io::stdin().read_line(&mut local)?; - let expanded = PathBuf::from(path::expand_env(&local.trim())?); - // We need to generate the directory beforehand to verify the path is - // actually valid. Worst case this leaves empty directories scattered in - // various locations after repeated initialization. - fs::create_dir_all(&expanded)?; - // Hard resolution should succeed now that the above directory was created. - Ok(path::resolve(&expanded)?) + Ok(PathBuf::from(path::expand_env(&local.trim())?)) } fn prompt_remote(config: &PathConfig) -> Result { @@ -118,8 +119,12 @@ pub fn write_config(mut pending: PathConfig) -> Result<()> { "Generating config at {}...\n", Green.paint(pending.0.unresolved().display().to_string()) ); - pending.1.local = Some(prompt_local(&pending)?); - pending.1.remote = prompt_remote(&pending)?; + let local = prompt_local(&pending)?; + let remote = prompt_remote(&pending)?; + // Try to initialize the local respository if we can. + let resolved = git::init(&local, &pending)?; + pending.1.local = Some(resolved); + pending.1.remote = remote; pending.write()?; println!("\nFinished writing configuration file."); Ok(()) diff --git a/src/daemon.rs b/src/daemon.rs index 783d0f2..175a6f6 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -13,6 +13,7 @@ use std::time::Duration; // TODO(jrpotter): Add logging. // TODO(jrpotter): Add pid file to only allow one daemon at a time. +// TODO(jrpotter): Sync files to local git repository. // ======================================== // Polling diff --git a/src/git.rs b/src/git.rs index a38a4b7..267bf94 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,4 +1,145 @@ use super::config::PathConfig; +use super::path; +use super::path::ResPathBuf; +use std::env::VarError; +use std::path::{Path, PathBuf}; +use std::{error, fmt, fs, io, 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 {} + +// ======================================== +// Validation +// ======================================== + +fn validate_is_file(path: &Path) -> Result<()> { + let metadata = fs::metadata(path)?; + if !metadata.is_file() { + // TODO(jrpotter): Use `IsADirectory` when stable. + Err(io::Error::new( + io::ErrorKind::Other, + format!("'{}' is not a file.", path.display()), + ))?; + } + Ok(()) +} + +fn validate_is_dir(path: &Path) -> Result<()> { + let metadata = fs::metadata(path)?; + if !metadata.is_dir() { + // TODO(jrpotter): Use `NotADirectory` when stable. + Err(io::Error::new( + io::ErrorKind::Other, + format!("'{}' is not a directory.", path.display()), + ))?; + } + Ok(()) +} + +pub fn validate_local(path: &Path) -> Result<()> { + let resolved = path::resolve(path)?; + validate_is_dir(resolved.as_ref())?; + + let mut local: PathBuf = resolved.into(); + local.push(".git"); + path::resolve(&local).map_err(|_| { + io::Error::new( + io::ErrorKind::NotFound, + format!( + "Local directory '{}' is not a git repository.", + path.display() + ), + ) + })?; + validate_is_dir(local.as_ref())?; + + local.pop(); + local.push(".homesync"); + path::resolve(&local).map_err(|_| { + io::Error::new( + io::ErrorKind::NotFound, + format!( + "Sentinel file '.homesync' missing from local repository '{}'.", + path.display() + ), + ) + })?; + validate_is_file(local.as_ref())?; + + // TODO(jrpotter): Verify git repository is pointing to remote. + + Ok(()) +} + +// ======================================== +// Repository +// ======================================== + +fn _setup_repo(path: &Path) -> Result<()> { + match path.parent() { + Some(p) => fs::create_dir_all(p)?, + None => (), + }; + let mut repo_dir = path.to_path_buf(); + repo_dir.push(".homesync"); + match path::soft_resolve(&repo_dir) { + // The path already exists. Verify we are working with a git respository + // with sentinel value. + Ok(Some(resolved)) => { + validate_local(resolved.as_ref())?; + } + // Path does not exist yet. If a remote path exists, we should clone it. + // Otherwise boot up a local repsoitory. + Ok(None) => {} + Err(e) => Err(e)?, + } + Ok(()) +} + +// ======================================== +// Initialization +// ======================================== /// Sets up a local github repository all configuration files will be synced to. /// We attempt to clone the remote repository in favor of building our own. @@ -11,6 +152,12 @@ use super::config::PathConfig; /// /// NOTE! This does not perform any syncing between local and remote. That /// should be done as a specific command line request. -pub async fn init(_config: &PathConfig) { - // TODO(jrpotter): Fill this out. +pub fn init(_path: &Path, _config: &PathConfig) -> Result { + // let repository = match Repository::clone(url, "/path/to/a/repo") { + // Ok(repo) => repo, + // Err(e) => panic!("failed to clone: {}", e), + // }; + // Hard resolution should succeed now that the above directory was created. + // Ok(path::resolve(&expanded)?); + panic!("") } diff --git a/src/main.rs b/src/main.rs index ef3c0cf..7ad3bc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,9 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box> { // used, even if one of higher priority is eventually defined. subcommand => { let config = homesync::config::load(&candidates)?; + if let Some(local) = &config.1.local { + homesync::git::validate_local(local.as_ref())?; + } match subcommand { Some(("add", _)) => Ok(homesync::run_add(config)?), Some(("daemon", matches)) => { diff --git a/src/path.rs b/src/path.rs index 55c12aa..e878cab 100644 --- a/src/path.rs +++ b/src/path.rs @@ -3,14 +3,10 @@ use serde::de; use serde::de::{Unexpected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::env::VarError; -use std::error; use std::ffi::OsString; -use std::fmt; use std::hash::{Hash, Hasher}; use std::path::{Component, Path, PathBuf}; -use std::result; -use std::str; -use std::{env, io}; +use std::{env, error, fmt, io, result, str}; // ======================================== // Error @@ -87,14 +83,14 @@ impl From for PathBuf { } } -impl AsRef for ResPathBuf { - fn as_ref(&self) -> &Path { +impl AsRef for ResPathBuf { + fn as_ref(&self) -> &PathBuf { &self.inner } } -impl AsRef for ResPathBuf { - fn as_ref(&self) -> &PathBuf { +impl AsRef for ResPathBuf { + fn as_ref(&self) -> &Path { &self.inner } }