parent
cbac40e4ef
commit
4bff745573
|
@ -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<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
IOError(io::Error),
|
||||||
|
VarError(VarError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(err: io::Error) -> Error {
|
||||||
|
Error::IOError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VarError> for Error {
|
||||||
|
fn from(err: VarError) -> Error {
|
||||||
|
Error::VarError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<path::Error> 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 <italic>git -C <green>{}</> <italic>status</> to see what changed.",
|
||||||
|
&pc.config.repos.local.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Utility
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
fn get_repo_lookup(root: &Path, path: &Path) -> Result<HashMap<PathBuf, ResPathBuf>> {
|
||||||
|
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<PathBuf, ResPathBuf> {
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
use super::{config, config::PathConfig, git, path, path::ResPathBuf};
|
use super::{config, config::PathConfig, copy, path, path::ResPathBuf};
|
||||||
use git2::Repository;
|
|
||||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use simplelog::{error, paris, trace, warn};
|
use simplelog::{error, paris, trace, warn};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -139,7 +138,7 @@ impl<'a> WatchState<'a> {
|
||||||
// Daemon
|
// Daemon
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
pub fn launch(mut pc: PathConfig, repo: Repository, 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();
|
||||||
|
@ -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)?;
|
let mut state = WatchState::new(poll_tx, &mut watcher)?;
|
||||||
state.update(&pc);
|
state.update(&pc);
|
||||||
loop {
|
loop {
|
||||||
git::stage(&pc, &repo)?;
|
copy::stage(&pc)?;
|
||||||
// Received paths should always be fully resolved.
|
// Received paths should always be fully resolved.
|
||||||
match watch_rx.recv() {
|
match watch_rx.recv() {
|
||||||
Ok(DebouncedEvent::NoticeWrite(p)) => {
|
Ok(DebouncedEvent::NoticeWrite(p)) => {
|
||||||
|
|
84
src/git.rs
84
src/git.rs
|
@ -4,12 +4,11 @@ use git2::{
|
||||||
ObjectType, PushOptions, Remote, RemoteCallbacks, Repository, Signature, StashApplyOptions,
|
ObjectType, PushOptions, Remote, RemoteCallbacks, Repository, Signature, StashApplyOptions,
|
||||||
StashFlags,
|
StashFlags,
|
||||||
};
|
};
|
||||||
use path::ResPathBuf;
|
|
||||||
use simplelog::{info, paris, warn};
|
use simplelog::{info, paris, warn};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
env::VarError,
|
env::VarError,
|
||||||
error, fmt, fs, io,
|
error, fmt, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
|
@ -135,87 +134,6 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Staging
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
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 (_, 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<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)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Staged files. Run <italic>git -C <green>{}</> <italic>status</> to see what changed.",
|
|
||||||
&pc.config.repos.local.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Syncing
|
// Syncing
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,4 +1,5 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod copy;
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
@ -8,9 +9,13 @@ use std::error::Error;
|
||||||
|
|
||||||
type Result = std::result::Result<(), Box<dyn Error>>;
|
type Result = std::result::Result<(), Box<dyn Error>>;
|
||||||
|
|
||||||
|
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 {
|
pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result {
|
||||||
let repo = git::init(&config)?;
|
daemon::launch(config, freq_secs)?;
|
||||||
daemon::launch(config, repo, freq_secs)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +37,6 @@ pub fn run_pull(config: PathConfig) -> Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_stage(config: PathConfig) -> Result {
|
pub fn run_stage(config: PathConfig) -> Result {
|
||||||
let repo = git::init(&config)?;
|
copy::stage(&config)?;
|
||||||
git::stage(&config, &repo)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -38,6 +38,25 @@ 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("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(
|
.subcommand(
|
||||||
App::new("daemon")
|
App::new("daemon")
|
||||||
.about("Start up a new homesync daemon")
|
.about("Start up a new homesync daemon")
|
||||||
|
@ -74,6 +93,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||||
let candidates = find_candidates(&matches)?;
|
let candidates = find_candidates(&matches)?;
|
||||||
let config = homesync::config::load(&candidates)?;
|
let config = homesync::config::load(&candidates)?;
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
|
Some(("apply", matches)) => Ok(homesync::run_apply(config, matches.value_of("file"))?),
|
||||||
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),
|
||||||
|
|
Loading…
Reference in New Issue