Allow specifying SSH keys and clean up config further.

pull/3/head
Joshua Potter 2022-01-07 12:30:46 -05:00
parent 9fdbb34967
commit 014a7d72e5
5 changed files with 178 additions and 131 deletions

View File

@ -2,39 +2,35 @@
user:
name: jrpotter
email: jrpotter@github.io
ssh:
private: $HOME/.ssh/id_ed25519
repos:
local: $HOME/.homesync
remote:
name: origin
branch: master
url: "https://github.com/jrpotter/home-config.git"
url: "git@github.com:jrpotter/home-config.git"
packages:
alacritty:
configs:
- $HOME/.alacritty.yml
- $HOME/.config/alacritty/alacritty.yml
- $XDG_CONFIG_HOME/alacritty.yml
- $XDG_CONFIG_HOME/alacritty/alacritty.yml
bash:
configs:
- $HOME/.bash_profile
- $HOME/.bashrc
home-manager:
configs:
- $HOME/home.nix
homesync:
configs:
- $HOME/.homesync.yml
- $HOME/.config/homesync/homesync.yml
- $XDG_CONFIG_HOME/homesync.yml
- $XDG_CONFIG_HOME/homesync/homesync.yml
neovim:
configs:
- $HOME/.config/nvim/init.vim
- $XDG_CONFIG_HOME/nvim/init.vim
termite:
configs:
- $HOME/.config/termite/config
- $XDG_CONFIG_HOME/termite/config
tmux:
configs:
- $HOME/.tmux.conf

View File

@ -2,6 +2,10 @@
user:
name: name
email: email@email.com
ssh:
public: $HOME/.ssh/id_ed25519.pub
private: $HOME/.ssh/id_ed25519
repos:
local: $HOME/.homesync
remote:
name: origin
@ -9,7 +13,6 @@ remote:
url: "https://github.com/owner/repo.git"
packages:
homesync:
configs:
- $HOME/.homesync.yml
- $HOME/.config/homesync/homesync.yml
- $XDG_CONFIG_HOME/homesync.yml

View File

@ -3,7 +3,6 @@ use paris::formatter::colorize_string;
use serde_derive::{Deserialize, Serialize};
use simplelog::{info, paris};
use std::{collections::BTreeMap, env::VarError, error, fmt, fs, io, io::Write, path::PathBuf};
use url::{ParseError, Url};
// ========================================
// Error
@ -15,7 +14,6 @@ pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {
IOError(io::Error),
MissingConfig,
ParseError(ParseError),
SerdeError(serde_yaml::Error),
VarError(VarError),
}
@ -41,12 +39,6 @@ impl From<path::Error> for Error {
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Error {
Error::ParseError(err)
}
}
impl From<VarError> for Error {
fn from(err: VarError) -> Error {
Error::VarError(err)
@ -58,7 +50,6 @@ impl fmt::Display for Error {
match self {
Error::IOError(e) => write!(f, "{}", e),
Error::MissingConfig => write!(f, "Could not find configuration file"),
Error::ParseError(e) => write!(f, "{}", e),
Error::SerdeError(e) => write!(f, "{}", e),
Error::VarError(e) => write!(f, "{}", e),
}
@ -78,23 +69,36 @@ pub struct User {
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Package {
pub configs: Vec<PathBuf>,
pub struct SSH {
pub public: Option<PathBuf>,
pub private: PathBuf,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Remote {
pub name: String,
pub branch: String,
pub url: Url,
pub url: String,
}
impl Remote {
pub fn tracking_branch(&self) -> String {
format!("{}/{}", &self.name, &self.branch)
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Repos {
pub local: PathBuf,
pub remote: Remote,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub user: User,
pub local: PathBuf,
pub remote: Remote,
pub packages: BTreeMap<String, Package>,
pub ssh: SSH,
pub repos: Repos,
pub packages: BTreeMap<String, Vec<PathBuf>>,
}
impl Config {
@ -160,7 +164,7 @@ pub fn load(candidates: &Vec<ResPathBuf>) -> Result<PathConfig> {
pub fn reload(pc: &PathConfig) -> Result<PathConfig> {
info!(
"<green>{}</> configuration reloaded.",
pc.config.local.display()
pc.config.repos.local.display()
);
load(&vec![pc.homesync_yml.clone()])
}

View File

@ -123,8 +123,8 @@ impl<'a> WatchState<'a> {
}
}
self.watching.clear();
for (_, package) in &pc.config.packages {
for path in &package.configs {
for (_, packages) in &pc.config.packages {
for path in packages {
match path::soft_resolve(&path) {
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
Ok(Some(n)) => self.watch(n),

View File

@ -1,10 +1,10 @@
use super::{config::PathConfig, path};
use git2::{
Branch, BranchType, Commit, DiffOptions, Direction, IndexAddOption, ObjectType, Remote,
Repository, Signature,
Branch, BranchType, Commit, Cred, DiffOptions, Direction, FetchOptions, Index, IndexAddOption,
ObjectType, Remote, RemoteCallbacks, Repository, Signature,
};
use path::ResPathBuf;
use simplelog::{info, paris, warn};
use simplelog::{info, paris};
use std::{
collections::HashSet,
env::VarError,
@ -75,30 +75,12 @@ impl error::Error for Error {}
// Initialization
// ========================================
fn clone_or_init(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
match Repository::clone(&pc.config.remote.url.to_string(), &expanded) {
Ok(repo) => {
info!(
"Cloned remote repository <green>{}</>.",
&pc.config.remote.url
);
Ok(repo)
}
Err(e) if e.code() == git2::ErrorCode::NotFound || e.code() == git2::ErrorCode::Auth => {
// TODO(jrpotter): Setup authentication callbacks so private
// repositories work.
// https://docs.rs/git2/0.13.25/git2/build/struct.RepoBuilder.html#example
if e.code() == git2::ErrorCode::Auth {
warn!("Could not authenticate against remote. Are you using a public repository?");
}
info!(
"Creating local repository at <green>{}</>.",
pc.config.local.display()
);
Ok(Repository::init(&expanded)?)
}
Err(e) => Err(e)?,
}
fn clone(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
let fetch_options = get_fetch_options(&pc)?;
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fetch_options);
Ok(builder.clone(&pc.config.repos.remote.url, &expanded)?)
}
/// Sets up a local github repository all configuration files will be synced to.
@ -112,7 +94,7 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
// 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 = path::expand(&pc.config.local)?;
let expanded = path::expand(&pc.config.repos.local)?;
// Attempt to open the local path as a git repository if possible. The
// `NotFound` error is thrown if:
//
@ -125,11 +107,29 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
Ok(repo) => {
info!(
"Opened local repository <green>{}</>.",
&pc.config.local.display()
&pc.config.repos.local.display()
);
Ok(repo)
}
Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(clone_or_init(&pc, &expanded)?),
Err(e) if e.code() == git2::ErrorCode::NotFound => match clone(&pc, &expanded) {
Ok(repo) => {
info!(
"Cloned remote repository <green>{}</>.",
&pc.config.repos.remote.url
);
Ok(repo)
}
Err(Error::GitError(e))
if e.code() == git2::ErrorCode::Eof && e.class() == git2::ErrorClass::Ssh =>
{
info!(
"Creating local repository at <green>{}</>.",
pc.config.repos.local.display()
);
Ok(Repository::init(&expanded)?)
}
Err(e) => Err(e)?,
},
Err(e) => Err(e)?,
}
}
@ -159,8 +159,8 @@ fn find_repo_files(path: &Path) -> Result<Vec<ResPathBuf>> {
fn find_package_files(pc: &PathConfig) -> Vec<ResPathBuf> {
let mut seen = Vec::new();
for (_, package) in &pc.config.packages {
for path in &package.configs {
for (_, packages) in &pc.config.packages {
for path in packages {
if let Ok(resolved) = path::resolve(path) {
seen.push(resolved);
}
@ -214,16 +214,11 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
// Syncing
// ========================================
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
// First pull to make sure there are no conflicts when we push our changes.
// This will also perform validation and construct our local and remote
// environment.
let _local_branch = pull(&pc, &repo)?;
let mut remote = get_remote(&pc, &repo)?;
// The index corresponds to our staging area. We add all files and write out
// to a tree. The resulting tree can be found using `git ls-tree <oid>`.
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
/// Adds all files to our index.
///
/// Checks explicitly if any changes have been detected in the newly constructed
/// index. If not, return `None`.
pub fn index_add(repo: &Repository) -> Result<Option<Index>> {
let mut index = repo.index()?;
index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?;
let diff_stats = repo
@ -240,11 +235,48 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
&& diff_stats.insertions() == 0
&& diff_stats.deletions() == 0
{
Ok(None)
} else {
Ok(Some(index))
}
}
/// Create or retrieve the remote specified within our configuration.
///
/// This method also configures the fetchspec for the remote, explicitly mapping
/// the remote branch against our local one.
///
/// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
repo.remote_set_url(&pc.config.repos.remote.name, &pc.config.repos.remote.url)?;
repo.remote_add_fetch(
&pc.config.repos.remote.name,
// We could go with "*" instead of {branch} for all remote branches.
&format!(
"+refs/heads/{}:refs/remotes/{}",
pc.config.repos.remote.branch,
pc.config.repos.remote.tracking_branch(),
),
)?;
Ok(repo.find_remote(&pc.config.repos.remote.name)?)
}
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
// First pull to make sure there are no conflicts when we push our changes.
// This will also perform validation and construct our local and remote
// environment.
let _local_branch = pull(&pc, &repo)?;
// The index corresponds to our staging area. We add all files and write out
// to a tree. The resulting tree can be found using `git ls-tree <oid>`.
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
let mut index = match index_add(&repo)? {
Some(index) => index,
None => {
info!("Nothing to push. Have you run `homesync stage`?");
return Ok(());
}
let signature = get_signature(&pc)?;
};
// Retrieve the latest commit before writing to the object database.
let parent_commit = get_commit(&repo)?;
let index_oid = index.write_tree()?;
@ -252,7 +284,8 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
info!("Writing index to tree `{}`.", index_oid);
// Commit our changes and push them to our remote.
let refspec = format!("refs/heads/{}", &pc.config.remote.branch);
let refspec = format!("refs/heads/{}", &pc.config.repos.remote.branch);
let signature = get_signature(&pc)?;
repo.commit(
Some(&refspec),
&signature,
@ -262,11 +295,13 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
&index_tree,
&[&parent_commit],
)?;
let mut remote = get_remote(&pc, &repo)?;
remote.connect(Direction::Push)?;
remote.push(&[&format!("{r}:{r}", r = refspec)], None)?;
info!(
"Pushed changes to remote `{}/{}`.",
&pc.config.remote.name, &pc.config.remote.branch
"Pushed changes to remote `{}`.",
pc.config.repos.remote.tracking_branch(),
);
Ok(())
@ -275,17 +310,25 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'repo>> {
validate_repo(&repo)?;
// TODO(jrpotter): Configure our remote to point to the same URL mentioned
// in our config.
// TODO(jrpotter): If changes are available, need to stage them and then
// reapply.
// Establish our remote. If the remote already exists, re-configure it
// blindly to point to the appropriate url. Our results should now exist
// in a branch called `remotes/origin/<branch>`.
// https://git-scm.com/book/it/v2/Git-Basics-Working-with-Remotes
// TODO(jrpotter): Configure our remote to point to the same URL mentioned
// in our config.
let mut remote = get_remote(&pc, &repo)?;
remote.fetch(&[&pc.config.remote.branch], None, None)?;
let remote_branch_name = format!("{}/{}", &pc.config.remote.name, &pc.config.remote.branch);
let mut fetch_options = get_fetch_options(&pc)?;
remote.fetch(
&[&pc.config.repos.remote.branch],
Some(&mut fetch_options),
None,
)?;
let remote_branch_name = pc.config.repos.remote.tracking_branch();
let remote_branch = repo.find_branch(&remote_branch_name, BranchType::Remote)?;
info!("Fetched remote branch `{}`.", remote_branch_name);
info!("Fetched remote branch `{}`.", &remote_branch_name);
// There are two cases we need to consider:
//
@ -293,11 +336,8 @@ pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'r
// available. These should be rebased relative to remote (our upstream).
// 2. Our repository has been initialized in an empty state. The branch we
// are interested in is unborn, so we can just copy the branch from remote.
//
// TODO(jrpotter): If changes are available, need to stage them and then
// reapply.
let remote_ref = repo.reference_to_annotated_commit(remote_branch.get())?;
if let Ok(local_branch) = repo.find_branch(&pc.config.remote.branch, BranchType::Local) {
if let Ok(local_branch) = repo.find_branch(&pc.config.repos.remote.branch, BranchType::Local) {
let local_ref = repo.reference_to_annotated_commit(local_branch.get())?;
let signature = get_signature(&pc)?;
repo.rebase(Some(&local_ref), Some(&remote_ref), None, None)?
@ -306,7 +346,7 @@ pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'r
Ok(local_branch)
} else {
let local_branch =
repo.branch_from_annotated_commit(&pc.config.remote.branch, &remote_ref, false)?;
repo.branch_from_annotated_commit(&pc.config.repos.remote.branch, &remote_ref, false)?;
info!("Created new local branch from `{}`.", remote_branch_name);
Ok(local_branch)
}
@ -332,23 +372,27 @@ fn get_commit(repo: &Repository) -> Result<Commit> {
.map_err(|_| git2::Error::from_str("Couldn't find commit"))?)
}
/// Create or retrieve the remote specified within our configuration.
///
/// This method also configures the fetchspec for the remote, explicitly mapping
/// the remote branch against our local one.
///
/// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
repo.remote_set_url(&pc.config.remote.name, &pc.config.remote.url.to_string())?;
repo.remote_add_fetch(
&pc.config.remote.name,
// We could go with "*" instead of {branch} for all remote branches.
&format!(
"+refs/heads/{branch}:refs/remotes/origin/{branch}",
branch = pc.config.remote.branch
),
)?;
Ok(repo.find_remote(&pc.config.remote.name)?)
/// Construct callbacks to supply authentication information on fetch/clone/etc.
fn get_fetch_options(pc: &PathConfig) -> Result<FetchOptions> {
let public_path = match &pc.config.ssh.public {
Some(p) => Some(path::resolve(p)?),
None => None,
};
let private_path = path::resolve(&pc.config.ssh.private)?;
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(move |_url, username_from_url, _allowed_types| {
Cred::ssh_key(
username_from_url.unwrap(),
public_path.as_ref().map(|p| p.resolved().as_ref()),
private_path.as_ref(),
None,
)
});
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
Ok(fetch_options)
}
/// Generate a new signature at the current time.