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 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<dyn Error>> {
|
||||
pub fn launch(mut pc: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
|
||||
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)) => {
|
||||
|
|
84
src/git.rs
84
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<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
|
||||
// ========================================
|
||||
|
|
12
src/lib.rs
12
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<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 {
|
||||
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(())
|
||||
}
|
||||
|
|
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")
|
||||
.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<dyn Error>> {
|
|||
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),
|
||||
|
|
Loading…
Reference in New Issue