From 2c95c431245e469aaad8bcef5c6b3ca642aaa6c8 Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Sat, 1 Jan 2022 12:10:57 -0500 Subject: [PATCH] Remove octocrab. Setup initialization of local git repository. --- Cargo.toml | 1 - examples/config.yaml | 2 +- src/config.rs | 16 ++-- src/daemon.rs | 20 +++-- src/git.rs | 187 +++++++++++++++++++++++-------------------- src/lib.rs | 21 ++--- src/main.rs | 4 +- src/path.rs | 24 ++++-- 8 files changed, 146 insertions(+), 129 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39f67d6..dd0041e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ ansi_term = "0.12.1" clap = { version = "3.0.0-rc.9", features = ["derive"] } git2 = "0.13.25" notify = "4.0.16" -octocrab = "0.15" regex = "1.5.4" serde = "1.0" serde_derive = "1.0.132" diff --git a/examples/config.yaml b/examples/config.yaml index c5ea7c8..3796fc1 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,5 +1,5 @@ --- -local: "" +local: $HOME/.homesync remote: "https://github.com/jrpotter/home-config.git" packages: homesync: diff --git a/src/config.rs b/src/config.rs index 85caf00..aa9b999 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,13 @@ -use super::path; -use super::path::ResPathBuf; +use super::{path, 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::{Path, PathBuf}; -use std::{error, fmt, fs, io}; +use std::{ + collections::BTreeMap, + env::VarError, + error, fmt, fs, io, + io::Write, + path::{Path, PathBuf}, +}; use url::{ParseError, Url}; // ======================================== @@ -210,7 +211,6 @@ pub fn write(path: &ResPathBuf, loaded: Option) -> Result { }, ); generated.write()?; - println!("\nFinished writing configuration file."); Ok(generated) } diff --git a/src/daemon.rs b/src/daemon.rs index 175a6f6..1c60d66 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,15 +1,13 @@ -use super::config; -use super::config::PathConfig; -use super::path; -use super::path::ResPathBuf; +use super::{config, config::PathConfig, path, path::ResPathBuf}; use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; -use std::collections::HashSet; -use std::error::Error; -use std::path::PathBuf; -use std::sync::mpsc::channel; -use std::sync::mpsc::{Receiver, Sender, TryRecvError}; -use std::thread; -use std::time::Duration; +use std::{ + collections::HashSet, + error::Error, + path::PathBuf, + sync::mpsc::{channel, Receiver, Sender, TryRecvError}, + thread, + time::Duration, +}; // TODO(jrpotter): Add logging. // TODO(jrpotter): Add pid file to only allow one daemon at a time. diff --git a/src/git.rs b/src/git.rs index 7a81add..d697ff7 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,9 +1,6 @@ -use super::config::PathConfig; -use super::path; -use super::path::ResPathBuf; -use std::env::VarError; -use std::path::{Path, PathBuf}; -use std::{error, fmt, fs, io, result}; +use super::{config::PathConfig, path}; +use git2::Repository; +use std::{env::VarError, error, fmt, fs, io, path::PathBuf, result}; // ======================================== // Error @@ -13,10 +10,18 @@ pub type Result = result::Result; #[derive(Debug)] pub enum Error { + GitError(git2::Error), IOError(io::Error), + NotHomesyncRepo, VarError(VarError), } +impl From for Error { + fn from(err: git2::Error) -> Error { + Error::GitError(err) + } +} + impl From for Error { fn from(err: io::Error) -> Error { Error::IOError(err) @@ -41,7 +46,12 @@ impl From for Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Error::GitError(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), } } @@ -49,91 +59,94 @@ impl fmt::Display 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 // ======================================== +// 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. -/// 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 -/// the presence of a sentinel file `.homesync`). Otherwise we raise an error. -/// -/// If there is no local repository but a remote is available, we clone it. -/// Otherwise we create a new, empty repository. -/// -/// NOTE! This does not perform any syncing between local and remote. That -/// should be done as a specific command line request. -pub fn init(_path: &Path, _config: &PathConfig) -> Result { - // let repository = match Repository::clone(url, "/path/to/a/repo") { - // Ok(repo) => repo, - // Err(e) => panic!("failed to clone: {}", e), - // }; - // Hard resolution should succeed now that the above directory was created. - // Ok(path::resolve(&expanded)?); - panic!("") +/// NOTE! This does not perform any syncing between local and remote. In fact, +/// this method does not perform any validation on the remote. +pub fn init(config: &PathConfig) -> Result { + // Permit the use of environment variables within the local configuration + // path (e.g. `$HOME`). Unlike with resolution, we want to fail if the + // environment variable is not defined. + let expanded = match config.1.local.to_str() { + 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 + // `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) + } + } } diff --git a/src/lib.rs b/src/lib.rs index bfc6e6a..564f3fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,7 @@ pub mod path; use config::PathConfig; use path::ResPathBuf; -use std::error::Error; -use std::io; +use std::{error::Error, io}; pub fn run_add(_config: PathConfig) -> Result<(), config::Error> { // TODO(jrpotter): Show $EDITOR that allows writing specific package. @@ -26,14 +25,11 @@ pub fn run_init(candidates: Vec) -> Result<(), Box> { "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 // the same configuration options and override the values present in the // current YAML file. - Ok(loaded) => { - config::write(&loaded.0, Some(loaded.1))?; - Ok(()) - } + Ok(loaded) => config::write(&loaded.0, Some(loaded.1))?, // 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 // not, the user should specify which config they want to write using @@ -41,11 +37,16 @@ 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() => { - config::write(&candidates[0], None)?; - Ok(()) + config::write(&candidates[0], None)? } 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> { diff --git a/src/main.rs b/src/main.rs index ef3c0cf..192ee28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ use clap::{App, AppSettings, Arg}; use homesync::path::ResPathBuf; -use std::error::Error; -use std::io; -use std::path::PathBuf; +use std::{error::Error, io, path::PathBuf}; fn main() { let matches = App::new("homesync") diff --git a/src/path.rs b/src/path.rs index 78ac49a..721237b 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,12 +1,20 @@ use regex::Regex; -use serde::de; -use serde::de::{Unexpected, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::env::VarError; -use std::ffi::OsString; -use std::hash::{Hash, Hasher}; -use std::path::{Component, Path, PathBuf}; -use std::{env, error, fmt, fs, io, result, str}; +use serde::{ + de, + de::{Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{ + env, + env::VarError, + error, + ffi::OsString, + fmt, fs, + hash::{Hash, Hasher}, + io, + path::{Component, Path, PathBuf}, + result, str, +}; // ======================================== // Error