Setup apply to sync between desktop and homesync repo.
Also remove unused commands.pull/3/head
parent
61b9a338a5
commit
bf65142e61
|
@ -14,7 +14,6 @@ clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
||||||
git2 = "0.13.25"
|
git2 = "0.13.25"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
notify = "4.0.16"
|
notify = "4.0.16"
|
||||||
regex = "1.5.4"
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0.132"
|
serde_derive = "1.0.132"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
|
|
|
@ -19,7 +19,11 @@
|
||||||
rustc
|
rustc
|
||||||
rustfmt
|
rustfmt
|
||||||
] ++ lib.optionals stdenv.isDarwin (
|
] ++ lib.optionals stdenv.isDarwin (
|
||||||
with darwin.apple_sdk.frameworks; [ CoreServices ]);
|
with darwin.apple_sdk.frameworks; [ CoreServices ]
|
||||||
|
) ++ lib.optionals stdenv.isLinux [
|
||||||
|
pkgs.openssl
|
||||||
|
pkgs.zlib
|
||||||
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,17 +96,23 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PathConfig(pub ResPathBuf, pub Config);
|
pub struct PathConfig {
|
||||||
|
pub homesync_yml: ResPathBuf,
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
impl PathConfig {
|
impl PathConfig {
|
||||||
pub fn new(path: &ResPathBuf, config: Config) -> Self {
|
pub fn new(path: &ResPathBuf, config: Config) -> Self {
|
||||||
PathConfig(path.clone(), config)
|
PathConfig {
|
||||||
|
homesync_yml: path.clone(),
|
||||||
|
config,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jrpotter): Create backup file before overwriting.
|
// TODO(jrpotter): Create backup file before overwriting.
|
||||||
pub fn write(&self) -> Result<()> {
|
pub fn write(&self) -> Result<()> {
|
||||||
let mut file = fs::File::create(&self.0)?;
|
let mut file = fs::File::create(&self.homesync_yml)?;
|
||||||
let serialized = serde_yaml::to_string(&self.1)?;
|
let serialized = serde_yaml::to_string(&self.config)?;
|
||||||
file.write_all(serialized.as_bytes())?;
|
file.write_all(serialized.as_bytes())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -143,12 +149,12 @@ pub fn load(candidates: &Vec<ResPathBuf>) -> Result<PathConfig> {
|
||||||
Err(Error::MissingConfig)
|
Err(Error::MissingConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reload(config: &PathConfig) -> Result<PathConfig> {
|
pub fn reload(pc: &PathConfig) -> Result<PathConfig> {
|
||||||
info!(
|
info!(
|
||||||
"<green>{}</> configuration reloaded.",
|
"<green>{}</> configuration reloaded.",
|
||||||
config.1.local.display()
|
pc.config.local.display()
|
||||||
);
|
);
|
||||||
load(&vec![config.0.clone()])
|
load(&vec![pc.homesync_yml.clone()])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
@ -205,14 +211,14 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
|
||||||
Some(c) => Some(&c.remote),
|
Some(c) => Some(&c.remote),
|
||||||
None => None,
|
None => None,
|
||||||
})?;
|
})?;
|
||||||
let generated = PathConfig(
|
let generated = PathConfig {
|
||||||
path.clone(),
|
homesync_yml: path.clone(),
|
||||||
Config {
|
config: Config {
|
||||||
local,
|
local,
|
||||||
remote,
|
remote,
|
||||||
packages: loaded.map_or(BTreeMap::new(), |c| c.packages),
|
packages: loaded.map_or(BTreeMap::new(), |c| c.packages),
|
||||||
},
|
},
|
||||||
);
|
};
|
||||||
generated.write()?;
|
generated.write()?;
|
||||||
Ok(generated)
|
Ok(generated)
|
||||||
}
|
}
|
||||||
|
@ -221,13 +227,16 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
|
||||||
// Listing
|
// Listing
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
pub fn list_packages(config: PathConfig) {
|
pub fn list_packages(pc: PathConfig) {
|
||||||
println!(
|
println!(
|
||||||
"Listing packages in {}...\n",
|
"Listing packages in {}...\n",
|
||||||
colorize_string(format!("<green>{}</>", config.0.unresolved().display())),
|
colorize_string(format!(
|
||||||
|
"<green>{}</>",
|
||||||
|
pc.homesync_yml.unresolved().display()
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
// Alphabetical ordered ensured by B-tree implementation.
|
// Alphabetical ordered ensured by B-tree implementation.
|
||||||
for (k, _) in config.1.packages {
|
for (k, _) in pc.config.packages {
|
||||||
println!("• {}", k);
|
println!("• {}", k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ impl<'a> WatchState<'a> {
|
||||||
|
|
||||||
/// Reads in the new path config, updating all watched and pending files
|
/// Reads in the new path config, updating all watched and pending files
|
||||||
/// according to the packages in the specified config.
|
/// according to the packages in the specified config.
|
||||||
pub fn update(&mut self, config: &PathConfig) {
|
pub fn update(&mut self, pc: &PathConfig) {
|
||||||
self.send_poll(PollEvent::Clear);
|
self.send_poll(PollEvent::Clear);
|
||||||
for path in &self.watching {
|
for path in &self.watching {
|
||||||
match self.watcher.unwatch(&path) {
|
match self.watcher.unwatch(&path) {
|
||||||
|
@ -122,7 +122,7 @@ impl<'a> WatchState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.watching.clear();
|
self.watching.clear();
|
||||||
for (_, package) in &config.1.packages {
|
for (_, package) in &pc.config.packages {
|
||||||
for path in &package.configs {
|
for path in &package.configs {
|
||||||
match path::soft_resolve(&path) {
|
match path::soft_resolve(&path) {
|
||||||
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
|
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
|
||||||
|
@ -138,7 +138,7 @@ impl<'a> WatchState<'a> {
|
||||||
// Daemon
|
// Daemon
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
pub fn launch(mut pc: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
||||||
let (poll_tx, poll_rx) = channel();
|
let (poll_tx, poll_rx) = channel();
|
||||||
let (watch_tx, watch_rx) = channel();
|
let (watch_tx, watch_rx) = channel();
|
||||||
let watch_tx1 = watch_tx.clone();
|
let watch_tx1 = watch_tx.clone();
|
||||||
|
@ -152,9 +152,9 @@ pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
|
||||||
// changes to it for hot reloading purposes, and not worry that our wrapper
|
// changes to it for hot reloading purposes, and not worry that our wrapper
|
||||||
// will ever clear it from its watch state.
|
// will ever clear it from its watch state.
|
||||||
let mut watcher: RecommendedWatcher = Watcher::new(watch_tx1, Duration::from_secs(freq_secs))?;
|
let mut watcher: RecommendedWatcher = Watcher::new(watch_tx1, Duration::from_secs(freq_secs))?;
|
||||||
watcher.watch(&config.0, RecursiveMode::NonRecursive)?;
|
watcher.watch(&pc.homesync_yml, RecursiveMode::NonRecursive)?;
|
||||||
let mut state = WatchState::new(poll_tx, &mut watcher)?;
|
let mut state = WatchState::new(poll_tx, &mut watcher)?;
|
||||||
state.update(&config);
|
state.update(&pc);
|
||||||
loop {
|
loop {
|
||||||
// Received paths should always be the fully resolved ones so safe to
|
// Received paths should always be the fully resolved ones so safe to
|
||||||
// compare against our current config path.
|
// compare against our current config path.
|
||||||
|
@ -166,16 +166,16 @@ pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
|
||||||
trace!("NoticeRemove {}", p.display());
|
trace!("NoticeRemove {}", p.display());
|
||||||
}
|
}
|
||||||
Ok(DebouncedEvent::Create(p)) => {
|
Ok(DebouncedEvent::Create(p)) => {
|
||||||
if config.0 == p {
|
if pc.homesync_yml == p {
|
||||||
config = config::reload(&config)?;
|
pc = config::reload(&pc)?;
|
||||||
state.update(&config);
|
state.update(&pc);
|
||||||
}
|
}
|
||||||
trace!("Create {}", p.display());
|
trace!("Create {}", p.display());
|
||||||
}
|
}
|
||||||
Ok(DebouncedEvent::Write(p)) => {
|
Ok(DebouncedEvent::Write(p)) => {
|
||||||
if config.0 == p {
|
if pc.homesync_yml == p {
|
||||||
config = config::reload(&config)?;
|
pc = config::reload(&pc)?;
|
||||||
state.update(&config);
|
state.update(&pc);
|
||||||
}
|
}
|
||||||
trace!("Write {}", p.display());
|
trace!("Write {}", p.display());
|
||||||
}
|
}
|
||||||
|
|
160
src/git.rs
160
src/git.rs
|
@ -1,7 +1,43 @@
|
||||||
use super::{config::PathConfig, path};
|
use super::{config::PathConfig, path};
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
|
use path::ResPathBuf;
|
||||||
use simplelog::{info, paris};
|
use simplelog::{info, paris};
|
||||||
use std::{env::VarError, error, fmt, fs, io, path::PathBuf, result};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
env::VarError,
|
||||||
|
error, fmt, fs, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// All git error codes.
|
||||||
|
// TODO(jrpotter): Remove these once done needing to reference them.
|
||||||
|
// git2::ErrorCode::GenericError => panic!("generic"),
|
||||||
|
// git2::ErrorCode::NotFound => panic!("not_found"),
|
||||||
|
// git2::ErrorCode::Exists => panic!("exists"),
|
||||||
|
// git2::ErrorCode::Ambiguous => panic!("ambiguous"),
|
||||||
|
// git2::ErrorCode::BufSize => panic!("buf_size"),
|
||||||
|
// git2::ErrorCode::User => panic!("user"),
|
||||||
|
// git2::ErrorCode::BareRepo => panic!("bare_repo"),
|
||||||
|
// git2::ErrorCode::UnbornBranch => panic!("unborn_branch"),
|
||||||
|
// git2::ErrorCode::Unmerged => panic!("unmerged"),
|
||||||
|
// git2::ErrorCode::NotFastForward => panic!("not_fast_forward"),
|
||||||
|
// git2::ErrorCode::InvalidSpec => panic!("invalid_spec"),
|
||||||
|
// git2::ErrorCode::Conflict => panic!("conflict"),
|
||||||
|
// git2::ErrorCode::Locked => panic!("locked"),
|
||||||
|
// git2::ErrorCode::Modified => panic!("modified"),
|
||||||
|
// git2::ErrorCode::Auth => panic!("auth"),
|
||||||
|
// git2::ErrorCode::Certificate => panic!("certificate"),
|
||||||
|
// git2::ErrorCode::Applied => panic!("applied"),
|
||||||
|
// git2::ErrorCode::Peel => panic!("peel"),
|
||||||
|
// git2::ErrorCode::Eof => panic!("eof"),
|
||||||
|
// git2::ErrorCode::Invalid => panic!("invalid"),
|
||||||
|
// git2::ErrorCode::Uncommitted => panic!("uncommitted"),
|
||||||
|
// git2::ErrorCode::Directory => panic!("directory"),
|
||||||
|
// git2::ErrorCode::MergeConflict => panic!("merge_conflict"),
|
||||||
|
// git2::ErrorCode::HashsumMismatch => panic!("hashsum_mismatch"),
|
||||||
|
// git2::ErrorCode::IndexDirty => panic!("index_dirty"),
|
||||||
|
// git2::ErrorCode::ApplyFail => panic!("apply_fail"),
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Error
|
// Error
|
||||||
|
@ -14,6 +50,7 @@ pub enum Error {
|
||||||
GitError(git2::Error),
|
GitError(git2::Error),
|
||||||
IOError(io::Error),
|
IOError(io::Error),
|
||||||
NotHomesyncRepo,
|
NotHomesyncRepo,
|
||||||
|
NotWorkingRepo,
|
||||||
VarError(VarError),
|
VarError(VarError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +90,10 @@ impl fmt::Display for Error {
|
||||||
f,
|
f,
|
||||||
"Local repository is not managed by `homesync`. Missing `.homesync` sentinel file."
|
"Local repository is not managed by `homesync`. Missing `.homesync` sentinel file."
|
||||||
),
|
),
|
||||||
|
Error::NotWorkingRepo => write!(
|
||||||
|
f,
|
||||||
|
"Local repository should be a working directory. Did you manually initialize with `--bare`?"
|
||||||
|
),
|
||||||
Error::VarError(e) => write!(f, "{}", e),
|
Error::VarError(e) => write!(f, "{}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,53 +105,17 @@ impl error::Error for Error {}
|
||||||
// Initialization
|
// Initialization
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
// All git error codes.
|
|
||||||
// TODO(jrpotter): Remove these once done needing to reference them.
|
|
||||||
// git2::ErrorCode::GenericError => panic!("generic"),
|
|
||||||
// git2::ErrorCode::NotFound => panic!("not_found"),
|
|
||||||
// git2::ErrorCode::Exists => panic!("exists"),
|
|
||||||
// git2::ErrorCode::Ambiguous => panic!("ambiguous"),
|
|
||||||
// git2::ErrorCode::BufSize => panic!("buf_size"),
|
|
||||||
// git2::ErrorCode::User => panic!("user"),
|
|
||||||
// git2::ErrorCode::BareRepo => panic!("bare_repo"),
|
|
||||||
// git2::ErrorCode::UnbornBranch => panic!("unborn_branch"),
|
|
||||||
// git2::ErrorCode::Unmerged => panic!("unmerged"),
|
|
||||||
// git2::ErrorCode::NotFastForward => panic!("not_fast_forward"),
|
|
||||||
// git2::ErrorCode::InvalidSpec => panic!("invalid_spec"),
|
|
||||||
// git2::ErrorCode::Conflict => panic!("conflict"),
|
|
||||||
// git2::ErrorCode::Locked => panic!("locked"),
|
|
||||||
// git2::ErrorCode::Modified => panic!("modified"),
|
|
||||||
// git2::ErrorCode::Auth => panic!("auth"),
|
|
||||||
// git2::ErrorCode::Certificate => panic!("certificate"),
|
|
||||||
// git2::ErrorCode::Applied => panic!("applied"),
|
|
||||||
// git2::ErrorCode::Peel => panic!("peel"),
|
|
||||||
// git2::ErrorCode::Eof => panic!("eof"),
|
|
||||||
// git2::ErrorCode::Invalid => panic!("invalid"),
|
|
||||||
// git2::ErrorCode::Uncommitted => panic!("uncommitted"),
|
|
||||||
// git2::ErrorCode::Directory => panic!("directory"),
|
|
||||||
// git2::ErrorCode::MergeConflict => panic!("merge_conflict"),
|
|
||||||
// git2::ErrorCode::HashsumMismatch => panic!("hashsum_mismatch"),
|
|
||||||
// git2::ErrorCode::IndexDirty => panic!("index_dirty"),
|
|
||||||
// git2::ErrorCode::ApplyFail => panic!("apply_fail"),
|
|
||||||
|
|
||||||
/// Sets up a local github repository all configuration files will be synced to.
|
/// Sets up a local github repository all configuration files will be synced to.
|
||||||
/// If there does not exist a local repository at the requested location, we
|
/// If there does not exist a local repository at the requested location, we
|
||||||
/// attempt to make it.
|
/// attempt to make it.
|
||||||
///
|
///
|
||||||
/// NOTE! This does not perform any syncing between local and remote. In fact,
|
/// NOTE! This does not perform any syncing between local and remote. In fact,
|
||||||
/// this method does not perform any validation on the remote.
|
/// this method does not perform any validation on remote at all.
|
||||||
pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
pub fn init(pc: &PathConfig) -> Result<Repository> {
|
||||||
// Permit the use of environment variables within the local configuration
|
// Permit the use of environment variables within the local configuration
|
||||||
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
|
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
|
||||||
// environment variable is not defined.
|
// environment variable is not defined.
|
||||||
let expanded = match config.1.local.to_str() {
|
let expanded = path::expand(&pc.config.local)?;
|
||||||
Some(s) => s,
|
|
||||||
None => Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Could not local path to a UTF-8 encoded string.",
|
|
||||||
))?,
|
|
||||||
};
|
|
||||||
let expanded = path::expand_env(expanded)?;
|
|
||||||
// Attempt to open the local path as a git repository if possible. The
|
// Attempt to open the local path as a git repository if possible. The
|
||||||
// `NotFound` error is thrown if:
|
// `NotFound` error is thrown if:
|
||||||
//
|
//
|
||||||
|
@ -146,7 +151,7 @@ pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
||||||
None => {
|
None => {
|
||||||
info!(
|
info!(
|
||||||
"Creating new homesync repository at <green>{}</>.",
|
"Creating new homesync repository at <green>{}</>.",
|
||||||
config.1.local.display()
|
pc.config.local.display()
|
||||||
);
|
);
|
||||||
let repo = Repository::init(&expanded)?;
|
let repo = Repository::init(&expanded)?;
|
||||||
fs::File::create(sentinel)?;
|
fs::File::create(sentinel)?;
|
||||||
|
@ -154,3 +159,76 @@ pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Application
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
fn find_repo_files(path: &Path) -> Result<Vec<ResPathBuf>> {
|
||||||
|
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<ResPathBuf> {
|
||||||
|
let mut seen = Vec::new();
|
||||||
|
for (_, package) in &pc.config.packages {
|
||||||
|
for path in &package.configs {
|
||||||
|
if let Ok(resolved) = path::resolve(path) {
|
||||||
|
seen.push(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
|
let workdir = repo.workdir().ok_or(Error::NotWorkingRepo)?;
|
||||||
|
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<PathBuf> = 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)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
26
src/lib.rs
26
src/lib.rs
|
@ -7,17 +7,20 @@ use config::PathConfig;
|
||||||
use path::ResPathBuf;
|
use path::ResPathBuf;
|
||||||
use std::{error::Error, io};
|
use std::{error::Error, io};
|
||||||
|
|
||||||
pub fn run_add(_config: PathConfig) -> Result<(), config::Error> {
|
type Result = std::result::Result<(), Box<dyn Error>>;
|
||||||
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
|
|
||||||
|
pub fn run_apply(config: PathConfig) -> Result {
|
||||||
|
let repo = git::init(&config)?;
|
||||||
|
git::apply(&config, &repo)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result {
|
||||||
daemon::launch(config, freq_secs)?;
|
daemon::launch(config, freq_secs)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result {
|
||||||
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
|
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
Err(config::Error::IOError(io::Error::new(
|
Err(config::Error::IOError(io::Error::new(
|
||||||
|
@ -29,7 +32,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
||||||
// Check if we already have a local config somewhere. If so, reprompt
|
// Check if we already have a local config somewhere. If so, reprompt
|
||||||
// the same configuration options and override the values present in the
|
// the same configuration options and override the values present in the
|
||||||
// current YAML file.
|
// current YAML file.
|
||||||
Ok(loaded) => config::write(&loaded.0, Some(loaded.1))?,
|
Ok(loaded) => config::write(&loaded.homesync_yml, Some(loaded.config))?,
|
||||||
// Otherwise create a new config file at the given location. We always
|
// Otherwise create a new config file at the given location. We always
|
||||||
// assume we want to write to the first file in our priority list. If
|
// assume we want to write to the first file in our priority list. If
|
||||||
// not, the user should specify which config they want to write using
|
// not, the user should specify which config they want to write using
|
||||||
|
@ -41,23 +44,12 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
Err(e) => Err(e)?,
|
Err(e) => Err(e)?,
|
||||||
};
|
};
|
||||||
// Verify (or create) our local and remote git repositories. The internal
|
|
||||||
// git library we chose to use employs async/await so let's wrap around a
|
|
||||||
// channel.
|
|
||||||
git::init(&config)?;
|
git::init(&config)?;
|
||||||
println!("\nFinished initialization.");
|
println!("\nFinished initialization.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
|
pub fn run_list(config: PathConfig) -> Result {
|
||||||
config::list_packages(config);
|
config::list_packages(config);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_pull(_config: PathConfig) -> Result<(), Box<dyn Error>> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_push(_config: PathConfig) -> Result<(), Box<dyn Error>> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -16,7 +16,7 @@ fn main() {
|
||||||
.expect("Could not initialize logger library.");
|
.expect("Could not initialize logger library.");
|
||||||
|
|
||||||
let matches = App::new("homesync")
|
let matches = App::new("homesync")
|
||||||
.about("Cross desktop configuration sync tool.")
|
.about("Cross desktop sync tool.")
|
||||||
.version("0.1.0")
|
.version("0.1.0")
|
||||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||||
.author("Joshua Potter <jrpotter.github.io>")
|
.author("Joshua Potter <jrpotter.github.io>")
|
||||||
|
@ -28,7 +28,9 @@ fn main() {
|
||||||
.help("Specify a configuration file to use in place of defaults")
|
.help("Specify a configuration file to use in place of defaults")
|
||||||
.takes_value(true),
|
.takes_value(true),
|
||||||
)
|
)
|
||||||
.subcommand(App::new("add").about("Add new configuration to local repository"))
|
.subcommand(
|
||||||
|
App::new("apply").about("Find all changes and apply them to the local repository"),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
App::new("daemon")
|
App::new("daemon")
|
||||||
.about("Start up a new homesync daemon")
|
.about("Start up a new homesync daemon")
|
||||||
|
@ -50,8 +52,6 @@ fn main() {
|
||||||
)
|
)
|
||||||
.subcommand(App::new("init").about("Initialize the homesync local repository"))
|
.subcommand(App::new("init").about("Initialize the homesync local repository"))
|
||||||
.subcommand(App::new("list").about("See which packages homesync manages"))
|
.subcommand(App::new("list").about("See which packages homesync manages"))
|
||||||
.subcommand(App::new("pull").about("Pull remote repository into local repository"))
|
|
||||||
.subcommand(App::new("push").about("Push local repository to remote repository"))
|
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
if let Err(e) = dispatch(matches) {
|
if let Err(e) = dispatch(matches) {
|
||||||
|
@ -71,7 +71,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||||
subcommand => {
|
subcommand => {
|
||||||
let config = homesync::config::load(&candidates)?;
|
let config = homesync::config::load(&candidates)?;
|
||||||
match subcommand {
|
match subcommand {
|
||||||
Some(("add", _)) => Ok(homesync::run_add(config)?),
|
Some(("apply", _)) => Ok(homesync::run_apply(config)?),
|
||||||
Some(("daemon", matches)) => {
|
Some(("daemon", matches)) => {
|
||||||
let freq_secs: u64 = match matches.value_of("frequency") {
|
let freq_secs: u64 = match matches.value_of("frequency") {
|
||||||
Some(f) => f.parse().unwrap_or(0),
|
Some(f) => f.parse().unwrap_or(0),
|
||||||
|
@ -85,8 +85,6 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(("list", _)) => Ok(homesync::run_list(config)?),
|
Some(("list", _)) => Ok(homesync::run_list(config)?),
|
||||||
Some(("pull", _)) => Ok(homesync::run_pull(config)?),
|
|
||||||
Some(("push", _)) => Ok(homesync::run_push(config)?),
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
62
src/path.rs
62
src/path.rs
|
@ -1,4 +1,3 @@
|
||||||
use regex::Regex;
|
|
||||||
use serde::{
|
use serde::{
|
||||||
de,
|
de,
|
||||||
de::{Unexpected, Visitor},
|
de::{Unexpected, Visitor},
|
||||||
|
@ -61,13 +60,30 @@ pub struct ResPathBuf {
|
||||||
unresolved: PathBuf,
|
unresolved: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unresolved_error(path: &Path) -> io::Error {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("Path '{}' should be fully resolved.", path.display()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
impl ResPathBuf {
|
impl ResPathBuf {
|
||||||
pub fn display(&self) -> std::path::Display {
|
pub fn new(path: &Path) -> Result<Self> {
|
||||||
self.inner.display()
|
if !path.is_absolute() {
|
||||||
|
Err(unresolved_error(path))?;
|
||||||
|
}
|
||||||
|
Ok(ResPathBuf {
|
||||||
|
inner: path.to_path_buf(),
|
||||||
|
unresolved: path.to_path_buf(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolved(&self) -> &PathBuf {
|
||||||
|
&self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unresolved(&self) -> &PathBuf {
|
pub fn unresolved(&self) -> &PathBuf {
|
||||||
return &self.unresolved;
|
&self.unresolved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,20 +230,8 @@ pub fn validate_is_dir(path: &Path) -> Result<()> {
|
||||||
/// Find environment variables within the argument and expand them if possible.
|
/// Find environment variables within the argument and expand them if possible.
|
||||||
///
|
///
|
||||||
/// Returns an error if any found environment variables are not defined.
|
/// Returns an error if any found environment variables are not defined.
|
||||||
pub fn expand_env(s: &str) -> Result<String> {
|
pub fn expand(path: &Path) -> Result<PathBuf> {
|
||||||
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap();
|
let mut expanded = env::current_dir()?;
|
||||||
let mut path = s.to_owned();
|
|
||||||
for caps in re.captures_iter(s) {
|
|
||||||
let evar = env::var(&caps["env"])?;
|
|
||||||
path = path.replace(&format!("${}", &caps["env"]), &evar);
|
|
||||||
}
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to resolve the provided path, returning a fully resolved path
|
|
||||||
/// instance if successful.
|
|
||||||
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
|
||||||
let mut resolved = env::current_dir()?;
|
|
||||||
for comp in path.components() {
|
for comp in path.components() {
|
||||||
match comp {
|
match comp {
|
||||||
Component::Prefix(_) => Err(io::Error::new(
|
Component::Prefix(_) => Err(io::Error::new(
|
||||||
|
@ -235,12 +239,12 @@ pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
||||||
"We do not currently support Windows.",
|
"We do not currently support Windows.",
|
||||||
))?,
|
))?,
|
||||||
Component::RootDir => {
|
Component::RootDir => {
|
||||||
resolved.clear();
|
expanded.clear();
|
||||||
resolved.push(Component::RootDir)
|
expanded.push(Component::RootDir)
|
||||||
}
|
}
|
||||||
Component::CurDir => (),
|
Component::CurDir => (),
|
||||||
Component::ParentDir => {
|
Component::ParentDir => {
|
||||||
if !resolved.pop() {
|
if !expanded.pop() {
|
||||||
Err(io::Error::new(
|
Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
"Cannot take parent of root.",
|
"Cannot take parent of root.",
|
||||||
|
@ -248,11 +252,23 @@ pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component::Normal(c) => {
|
Component::Normal(c) => {
|
||||||
let c: OsString = expand_env(&c.to_string_lossy())?.into();
|
let lossy = c.to_string_lossy();
|
||||||
resolved.push(Component::Normal(&c));
|
if lossy.starts_with("$") {
|
||||||
|
let evar = env::var(lossy.replacen("$", "", 1))?;
|
||||||
|
expanded.push(Component::Normal(&OsString::from(evar)));
|
||||||
|
} else {
|
||||||
|
expanded.push(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to resolve the provided path, returning a fully resolved path
|
||||||
|
/// instance if successful.
|
||||||
|
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
|
||||||
|
let resolved = expand(&path)?;
|
||||||
let resolved = resolved.canonicalize()?;
|
let resolved = resolved.canonicalize()?;
|
||||||
Ok(ResPathBuf {
|
Ok(ResPathBuf {
|
||||||
inner: resolved,
|
inner: resolved,
|
||||||
|
|
Loading…
Reference in New Issue