diff --git a/examples/config.yaml b/examples/config.yaml index 466e849..c5ea7c8 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,5 +1,6 @@ --- -remote: https://github.com/jrpotter/home-config.git +local: "" +remote: "https://github.com/jrpotter/home-config.git" packages: homesync: configs: diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 5847912..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,142 +0,0 @@ -use super::config::PathConfig; -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, io}; -use url::{ParseError, Url}; - -// TODO(jrpotter): Use curses to make this module behave nicer. - -// ======================================== -// Error -// ======================================== - -pub type Result = std::result::Result; - -#[derive(Debug)] -pub enum Error { - ConfigError(config::Error), - IOError(io::Error), - ParseError(ParseError), - VarError(VarError), -} - -impl From for Error { - fn from(err: config::Error) -> Error { - Error::ConfigError(err) - } -} - -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) - } -} - -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 From for Error { - fn from(err: ParseError) -> Error { - Error::ParseError(err) - } -} - -impl From for Error { - fn from(err: VarError) -> Error { - Error::VarError(err) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::ConfigError(e) => write!(f, "{}", e), - Error::IOError(e) => write!(f, "{}", e), - Error::ParseError(e) => write!(f, "{}", e), - Error::VarError(e) => write!(f, "{}", e), - } - } -} - -impl error::Error for Error {} - -// ======================================== -// Prompts -// ======================================== - -fn prompt_local(config: &PathConfig) -> Result { - print!( - "Local git repository <{}> (enter to continue): ", - Yellow.paint( - config - .1 - .local - .as_ref() - .map_or("".to_owned(), |v| v.display().to_string()) - ) - ); - io::stdout().flush()?; - let mut local = String::new(); - io::stdin().read_line(&mut local)?; - Ok(PathBuf::from(path::expand_env(&local.trim())?)) -} - -fn prompt_remote(config: &PathConfig) -> Result { - print!( - "Remote git repository <{}> (enter to continue): ", - Yellow.paint(config.1.remote.to_string()) - ); - io::stdout().flush()?; - let mut remote = String::new(); - io::stdin().read_line(&mut remote)?; - Ok(Url::parse(&remote)?) -} - -// ======================================== -// CLI -// ======================================== - -pub fn write_config(mut pending: PathConfig) -> Result<()> { - println!( - "Generating config at {}...\n", - Green.paint(pending.0.unresolved().display().to_string()) - ); - 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(()) -} - -pub fn list_packages(config: PathConfig) { - println!( - "Listing packages in {}...\n", - Green.paint(config.0.unresolved().display().to_string()) - ); - // Alphabetical ordered ensured by B-tree implementation. - for (k, _) in config.1.packages { - println!("• {}", k); - } -} diff --git a/src/config.rs b/src/config.rs index 4c6b62e..85caf00 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,13 @@ +use super::path; use super::path::ResPathBuf; +use ansi_term::Colour::{Green, Yellow}; use serde_derive::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::env::VarError; use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::{error, fmt, fs, io}; -use url::Url; +use url::{ParseError, Url}; // ======================================== // Error @@ -14,14 +17,16 @@ pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { - FileError(io::Error), + IOError(io::Error), MissingConfig, + ParseError(ParseError), SerdeError(serde_yaml::Error), + VarError(VarError), } impl From for Error { fn from(err: io::Error) -> Error { - Error::FileError(err) + Error::IOError(err) } } @@ -31,12 +36,35 @@ impl From for Error { } } +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 From for Error { + fn from(err: ParseError) -> Error { + Error::ParseError(err) + } +} + +impl From for Error { + fn from(err: VarError) -> Error { + Error::VarError(err) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::FileError(e) => write!(f, "{}", e), + Error::IOError(e) => write!(f, "{}", e), Error::MissingConfig => write!(f, "Could not find configuration file"), + Error::ParseError(e) => write!(f, "{}", e), Error::SerdeError(e) => write!(f, "{}", e), + Error::VarError(e) => write!(f, "{}", e), } } } @@ -54,7 +82,7 @@ pub struct Package { #[derive(Debug, Deserialize, Serialize)] pub struct Config { - pub local: Option, + pub local: PathBuf, pub remote: Url, pub packages: BTreeMap, } @@ -69,15 +97,8 @@ impl Config { pub struct PathConfig(pub ResPathBuf, pub Config); impl PathConfig { - pub fn new(path: &ResPathBuf, config: Option) -> Self { - PathConfig( - path.clone(), - config.unwrap_or(Config { - local: None, - remote: Url::parse("http://github.com/user/repo.git").unwrap(), - packages: BTreeMap::new(), - }), - ) + pub fn new(path: &ResPathBuf, config: Config) -> Self { + PathConfig(path.clone(), config) } // TODO(jrpotter): Create backup file before overwriting. @@ -110,10 +131,10 @@ pub fn load(candidates: &Vec) -> Result { for candidate in candidates { match fs::read_to_string(candidate) { Err(err) if err.kind() == io::ErrorKind::NotFound => continue, - Err(err) => return Err(Error::FileError(err)), + Err(err) => Err(Error::IOError(err))?, Ok(contents) => { let config = Config::new(&contents)?; - return Ok(PathConfig::new(candidate, Some(config))); + return Ok(PathConfig::new(candidate, config)); } } } @@ -125,3 +146,85 @@ pub fn reload(config: &PathConfig) -> Result { println!("Configuration reloaded."); load(&vec![config.0.clone()]) } + +// ======================================== +// Creation +// ======================================== + +fn prompt_local(path: Option<&Path>) -> Result { + let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string()); + print!( + "Local git repository <{}> (enter to continue): ", + Yellow.paint(&default) + ); + io::stdout().flush()?; + let mut local = String::new(); + io::stdin().read_line(&mut local)?; + // Defer validation this path until initialization of the repository. + let local = local.trim(); + if local.is_empty() { + Ok(PathBuf::from(default)) + } else { + Ok(PathBuf::from(local)) + } +} + +fn prompt_remote(url: Option<&Url>) -> Result { + let default = url.map_or("https://github.com/owner/repo.git".to_owned(), |u| { + u.to_string() + }); + print!( + "Remote git repository <{}> (enter to continue): ", + Yellow.paint(&default) + ); + io::stdout().flush()?; + let mut remote = String::new(); + io::stdin().read_line(&mut remote)?; + let remote = remote.trim(); + if remote.is_empty() { + Ok(Url::parse(&default)?) + } else { + Ok(Url::parse(&remote)?) + } +} + +pub fn write(path: &ResPathBuf, loaded: Option) -> Result { + println!( + "Generating config at {}...\n", + Green.paint(path.unresolved().display().to_string()) + ); + let local = prompt_local(match &loaded { + Some(c) => Some(c.local.as_ref()), + None => None, + })?; + let remote = prompt_remote(match &loaded { + Some(c) => Some(&c.remote), + None => None, + })?; + let generated = PathConfig( + path.clone(), + Config { + local, + remote, + packages: loaded.map_or(BTreeMap::new(), |c| c.packages), + }, + ); + generated.write()?; + println!("\nFinished writing configuration file."); + Ok(generated) +} + +// ======================================== +// Listing +// ======================================== + +pub fn list_packages(config: PathConfig) { + println!( + "Listing packages in {}...\n", + Green.paint(config.0.unresolved().display().to_string()) + ); + // Alphabetical ordered ensured by B-tree implementation. + for (k, _) in config.1.packages { + println!("• {}", k); + } +} diff --git a/src/git.rs b/src/git.rs index 267bf94..7a81add 100644 --- a/src/git.rs +++ b/src/git.rs @@ -53,33 +53,9 @@ 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())?; + path::validate_is_dir(resolved.as_ref())?; let mut local: PathBuf = resolved.into(); local.push(".git"); @@ -92,7 +68,7 @@ pub fn validate_local(path: &Path) -> Result<()> { ), ) })?; - validate_is_dir(local.as_ref())?; + path::validate_is_dir(local.as_ref())?; local.pop(); local.push(".homesync"); @@ -105,7 +81,7 @@ pub fn validate_local(path: &Path) -> Result<()> { ), ) })?; - validate_is_file(local.as_ref())?; + path::validate_is_file(local.as_ref())?; // TODO(jrpotter): Verify git repository is pointing to remote. diff --git a/src/lib.rs b/src/lib.rs index 2feb318..bfc6e6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -pub mod cli; pub mod config; pub mod daemon; pub mod git; @@ -22,7 +21,7 @@ pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box) -> Result<(), Box> { debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`."); if candidates.is_empty() { - Err(config::Error::FileError(io::Error::new( + Err(config::Error::IOError(io::Error::new( io::ErrorKind::NotFound, "No suitable config file found.", )))?; @@ -31,8 +30,8 @@ pub fn run_init(candidates: Vec) -> Result<(), Box> { // Check if we already have a local config somewhere. If so, reprompt // the same configuration options and override the values present in the // current YAML file. - Ok(pending) => { - cli::write_config(pending)?; + Ok(loaded) => { + config::write(&loaded.0, Some(loaded.1))?; Ok(()) } // Otherwise create a new config file at the given location. We always @@ -42,8 +41,7 @@ pub fn run_init(candidates: Vec) -> Result<(), Box> { // TODO(jrpotter): Verify I have permission to write at specified path. // Make directories if necessary. Err(config::Error::MissingConfig) if !candidates.is_empty() => { - let pending = PathConfig::new(&candidates[0], None); - cli::write_config(pending)?; + config::write(&candidates[0], None)?; Ok(()) } Err(e) => Err(e)?, @@ -51,7 +49,7 @@ pub fn run_init(candidates: Vec) -> Result<(), Box> { } pub fn run_list(config: PathConfig) -> Result<(), config::Error> { - cli::list_packages(config); + config::list_packages(config); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 7ad3bc8..ef3c0cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,9 +60,6 @@ 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 e878cab..78ac49a 100644 --- a/src/path.rs +++ b/src/path.rs @@ -6,7 +6,7 @@ use std::env::VarError; use std::ffi::OsString; use std::hash::{Hash, Hasher}; use std::path::{Component, Path, PathBuf}; -use std::{env, error, fmt, io, result, str}; +use std::{env, error, fmt, fs, io, result, str}; // ======================================== // Error @@ -171,6 +171,34 @@ impl<'de> Deserialize<'de> for ResPathBuf { } } +// ======================================== +// Validation +// ======================================== + +pub 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(()) +} + +pub 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(()) +} + // ======================================== // Resolution // ========================================