parent
d4183f2b52
commit
2c95c43124
|
@ -14,7 +14,6 @@ ansi_term = "0.12.1"
|
||||||
clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
||||||
git2 = "0.13.25"
|
git2 = "0.13.25"
|
||||||
notify = "4.0.16"
|
notify = "4.0.16"
|
||||||
octocrab = "0.15"
|
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0.132"
|
serde_derive = "1.0.132"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
local: ""
|
local: $HOME/.homesync
|
||||||
remote: "https://github.com/jrpotter/home-config.git"
|
remote: "https://github.com/jrpotter/home-config.git"
|
||||||
packages:
|
packages:
|
||||||
homesync:
|
homesync:
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use super::path;
|
use super::{path, path::ResPathBuf};
|
||||||
use super::path::ResPathBuf;
|
|
||||||
use ansi_term::Colour::{Green, Yellow};
|
use ansi_term::Colour::{Green, Yellow};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::{
|
||||||
use std::env::VarError;
|
collections::BTreeMap,
|
||||||
use std::io::Write;
|
env::VarError,
|
||||||
use std::path::{Path, PathBuf};
|
error, fmt, fs, io,
|
||||||
use std::{error, fmt, fs, io};
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
@ -210,7 +211,6 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
generated.write()?;
|
generated.write()?;
|
||||||
println!("\nFinished writing configuration file.");
|
|
||||||
Ok(generated)
|
Ok(generated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
use super::config;
|
use super::{config, config::PathConfig, path, path::ResPathBuf};
|
||||||
use super::config::PathConfig;
|
|
||||||
use super::path;
|
|
||||||
use super::path::ResPathBuf;
|
|
||||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use std::collections::HashSet;
|
use std::{
|
||||||
use std::error::Error;
|
collections::HashSet,
|
||||||
use std::path::PathBuf;
|
error::Error,
|
||||||
use std::sync::mpsc::channel;
|
path::PathBuf,
|
||||||
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
|
sync::mpsc::{channel, Receiver, Sender, TryRecvError},
|
||||||
use std::thread;
|
thread,
|
||||||
use std::time::Duration;
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
// TODO(jrpotter): Add logging.
|
// TODO(jrpotter): Add logging.
|
||||||
// TODO(jrpotter): Add pid file to only allow one daemon at a time.
|
// TODO(jrpotter): Add pid file to only allow one daemon at a time.
|
||||||
|
|
187
src/git.rs
187
src/git.rs
|
@ -1,9 +1,6 @@
|
||||||
use super::config::PathConfig;
|
use super::{config::PathConfig, path};
|
||||||
use super::path;
|
use git2::Repository;
|
||||||
use super::path::ResPathBuf;
|
use std::{env::VarError, error, fmt, fs, io, path::PathBuf, result};
|
||||||
use std::env::VarError;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::{error, fmt, fs, io, result};
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Error
|
// Error
|
||||||
|
@ -13,10 +10,18 @@ pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
GitError(git2::Error),
|
||||||
IOError(io::Error),
|
IOError(io::Error),
|
||||||
|
NotHomesyncRepo,
|
||||||
VarError(VarError),
|
VarError(VarError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<git2::Error> for Error {
|
||||||
|
fn from(err: git2::Error) -> Error {
|
||||||
|
Error::GitError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(err: io::Error) -> Error {
|
fn from(err: io::Error) -> Error {
|
||||||
Error::IOError(err)
|
Error::IOError(err)
|
||||||
|
@ -41,7 +46,12 @@ impl From<path::Error> for Error {
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Error::GitError(e) => write!(f, "{}", e),
|
||||||
Error::IOError(e) => write!(f, "{}", e),
|
Error::IOError(e) => write!(f, "{}", e),
|
||||||
|
Error::NotHomesyncRepo => write!(
|
||||||
|
f,
|
||||||
|
"Local repository is not managed by `homesync`. Missing `.homesync` sentinel file."
|
||||||
|
),
|
||||||
Error::VarError(e) => write!(f, "{}", e),
|
Error::VarError(e) => write!(f, "{}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,91 +59,94 @@ impl fmt::Display for Error {
|
||||||
|
|
||||||
impl error::Error for Error {}
|
impl error::Error for Error {}
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Validation
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
pub fn validate_local(path: &Path) -> Result<()> {
|
|
||||||
let resolved = path::resolve(path)?;
|
|
||||||
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()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
path::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()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
path::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
|
// 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.
|
||||||
/// We attempt to clone the remote repository in favor of building our own.
|
/// If there does not exist a local repository at the requested location, we
|
||||||
|
/// attempt to make it.
|
||||||
///
|
///
|
||||||
/// If a remote repository exists, we verify its managed by homesync (based on
|
/// NOTE! This does not perform any syncing between local and remote. In fact,
|
||||||
/// the presence of a sentinel file `.homesync`). Otherwise we raise an error.
|
/// this method does not perform any validation on the remote.
|
||||||
///
|
pub fn init(config: &PathConfig) -> Result<git2::Repository> {
|
||||||
/// If there is no local repository but a remote is available, we clone it.
|
// Permit the use of environment variables within the local configuration
|
||||||
/// Otherwise we create a new, empty repository.
|
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
|
||||||
///
|
// environment variable is not defined.
|
||||||
/// NOTE! This does not perform any syncing between local and remote. That
|
let expanded = match config.1.local.to_str() {
|
||||||
/// should be done as a specific command line request.
|
Some(s) => s,
|
||||||
pub fn init(_path: &Path, _config: &PathConfig) -> Result<ResPathBuf> {
|
None => Err(io::Error::new(
|
||||||
// let repository = match Repository::clone(url, "/path/to/a/repo") {
|
io::ErrorKind::InvalidInput,
|
||||||
// Ok(repo) => repo,
|
"Could not local path to a UTF-8 encoded string.",
|
||||||
// Err(e) => panic!("failed to clone: {}", e),
|
))?,
|
||||||
// };
|
};
|
||||||
// Hard resolution should succeed now that the above directory was created.
|
let expanded = path::expand_env(expanded)?;
|
||||||
// Ok(path::resolve(&expanded)?);
|
// Attempt to open the local path as a git repository if possible. The
|
||||||
panic!("")
|
// `NotFound` error is thrown if:
|
||||||
|
//
|
||||||
|
// - the directory does not exist.
|
||||||
|
// - the directory is not git-initialized (i.e. has a valid `.git`
|
||||||
|
// subfolder).
|
||||||
|
// - the directory does not have appropriate permissions.
|
||||||
|
let local = match Repository::open(&expanded) {
|
||||||
|
Ok(repo) => Some(repo),
|
||||||
|
Err(e) => match e.code() {
|
||||||
|
git2::ErrorCode::NotFound => None,
|
||||||
|
_ => Err(e)?,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Setup a sentinel file in the given repository. This is used for both
|
||||||
|
// ensuring any remote repositories are already managed by homesync and for
|
||||||
|
// storing any persisted configurations.
|
||||||
|
let mut sentinel = PathBuf::from(&expanded);
|
||||||
|
sentinel.push(".homesync");
|
||||||
|
match local {
|
||||||
|
Some(repo) => {
|
||||||
|
// Verify the given repository has a homesync sentinel file.
|
||||||
|
match path::validate_is_file(&sentinel) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => Err(Error::NotHomesyncRepo)?,
|
||||||
|
};
|
||||||
|
Ok(repo)
|
||||||
|
}
|
||||||
|
// If no local repository exists, we choose to just always initialize a
|
||||||
|
// new one instead of cloning from remote. Cloning has a separate set of
|
||||||
|
// issues that we need to resolve anyways (e.g. setting remote, pulling,
|
||||||
|
// managing possible merge conflicts, etc.).
|
||||||
|
None => {
|
||||||
|
println!("Creating new homesync repository.");
|
||||||
|
let repo = Repository::init(&expanded)?;
|
||||||
|
fs::File::create(sentinel)?;
|
||||||
|
Ok(repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
src/lib.rs
21
src/lib.rs
|
@ -5,8 +5,7 @@ pub mod path;
|
||||||
|
|
||||||
use config::PathConfig;
|
use config::PathConfig;
|
||||||
use path::ResPathBuf;
|
use path::ResPathBuf;
|
||||||
use std::error::Error;
|
use std::{error::Error, io};
|
||||||
use std::io;
|
|
||||||
|
|
||||||
pub fn run_add(_config: PathConfig) -> Result<(), config::Error> {
|
pub fn run_add(_config: PathConfig) -> Result<(), config::Error> {
|
||||||
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
|
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
|
||||||
|
@ -26,14 +25,11 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
||||||
"No suitable config file found.",
|
"No suitable config file found.",
|
||||||
)))?;
|
)))?;
|
||||||
}
|
}
|
||||||
match config::load(&candidates) {
|
let config = match config::load(&candidates) {
|
||||||
// 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) => {
|
Ok(loaded) => config::write(&loaded.0, Some(loaded.1))?,
|
||||||
config::write(&loaded.0, Some(loaded.1))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
// 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,11 +37,16 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
|
||||||
// TODO(jrpotter): Verify I have permission to write at specified path.
|
// TODO(jrpotter): Verify I have permission to write at specified path.
|
||||||
// Make directories if necessary.
|
// Make directories if necessary.
|
||||||
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
|
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
|
||||||
config::write(&candidates[0], None)?;
|
config::write(&candidates[0], None)?
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
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)?;
|
||||||
|
println!("Finished initialization.");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
|
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use clap::{App, AppSettings, Arg};
|
use clap::{App, AppSettings, Arg};
|
||||||
use homesync::path::ResPathBuf;
|
use homesync::path::ResPathBuf;
|
||||||
use std::error::Error;
|
use std::{error::Error, io, path::PathBuf};
|
||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = App::new("homesync")
|
let matches = App::new("homesync")
|
||||||
|
|
24
src/path.rs
24
src/path.rs
|
@ -1,12 +1,20 @@
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::de;
|
use serde::{
|
||||||
use serde::de::{Unexpected, Visitor};
|
de,
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
de::{Unexpected, Visitor},
|
||||||
use std::env::VarError;
|
Deserialize, Deserializer, Serialize, Serializer,
|
||||||
use std::ffi::OsString;
|
};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::{
|
||||||
use std::path::{Component, Path, PathBuf};
|
env,
|
||||||
use std::{env, error, fmt, fs, io, result, str};
|
env::VarError,
|
||||||
|
error,
|
||||||
|
ffi::OsString,
|
||||||
|
fmt, fs,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
io,
|
||||||
|
path::{Component, Path, PathBuf},
|
||||||
|
result, str,
|
||||||
|
};
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Error
|
// Error
|
||||||
|
|
Loading…
Reference in New Issue