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: user:
name: jrpotter name: jrpotter
email: jrpotter@github.io email: jrpotter@github.io
local: $HOME/.homesync ssh:
remote: private: $HOME/.ssh/id_ed25519
name: origin repos:
branch: master local: $HOME/.homesync
url: "https://github.com/jrpotter/home-config.git" remote:
name: origin
branch: master
url: "git@github.com:jrpotter/home-config.git"
packages: packages:
alacritty: alacritty:
configs: - $HOME/.alacritty.yml
- $HOME/.alacritty.yml - $HOME/.config/alacritty/alacritty.yml
- $HOME/.config/alacritty/alacritty.yml - $XDG_CONFIG_HOME/alacritty.yml
- $XDG_CONFIG_HOME/alacritty.yml - $XDG_CONFIG_HOME/alacritty/alacritty.yml
- $XDG_CONFIG_HOME/alacritty/alacritty.yml
bash: bash:
configs: - $HOME/.bash_profile
- $HOME/.bash_profile - $HOME/.bashrc
- $HOME/.bashrc
home-manager: home-manager:
configs: - $HOME/home.nix
- $HOME/home.nix
homesync: homesync:
configs: - $HOME/.homesync.yml
- $HOME/.homesync.yml - $HOME/.config/homesync/homesync.yml
- $HOME/.config/homesync/homesync.yml - $XDG_CONFIG_HOME/homesync.yml
- $XDG_CONFIG_HOME/homesync.yml - $XDG_CONFIG_HOME/homesync/homesync.yml
- $XDG_CONFIG_HOME/homesync/homesync.yml
neovim: neovim:
configs: - $HOME/.config/nvim/init.vim
- $HOME/.config/nvim/init.vim - $XDG_CONFIG_HOME/nvim/init.vim
- $XDG_CONFIG_HOME/nvim/init.vim
termite: termite:
configs: - $HOME/.config/termite/config
- $HOME/.config/termite/config - $XDG_CONFIG_HOME/termite/config
- $XDG_CONFIG_HOME/termite/config
tmux: tmux:
configs: - $HOME/.tmux.conf
- $HOME/.tmux.conf

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
use super::{config::PathConfig, path}; use super::{config::PathConfig, path};
use git2::{ use git2::{
Branch, BranchType, Commit, DiffOptions, Direction, IndexAddOption, ObjectType, Remote, Branch, BranchType, Commit, Cred, DiffOptions, Direction, FetchOptions, Index, IndexAddOption,
Repository, Signature, ObjectType, Remote, RemoteCallbacks, Repository, Signature,
}; };
use path::ResPathBuf; use path::ResPathBuf;
use simplelog::{info, paris, warn}; use simplelog::{info, paris};
use std::{ use std::{
collections::HashSet, collections::HashSet,
env::VarError, env::VarError,
@ -75,30 +75,12 @@ impl error::Error for Error {}
// Initialization // Initialization
// ======================================== // ========================================
fn clone_or_init(pc: &PathConfig, expanded: &Path) -> Result<Repository> { fn clone(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
match Repository::clone(&pc.config.remote.url.to_string(), &expanded) { let fetch_options = get_fetch_options(&pc)?;
Ok(repo) => { let mut builder = git2::build::RepoBuilder::new();
info!( builder.fetch_options(fetch_options);
"Cloned remote repository <green>{}</>.",
&pc.config.remote.url Ok(builder.clone(&pc.config.repos.remote.url, &expanded)?)
);
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)?,
}
} }
/// 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.
@ -112,7 +94,7 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
// Permit the use of environment variables within the local configuration // Permit the use of environment variables within the local configuration
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the // path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
// environment variable is not defined. // 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 // Attempt to open the local path as a git repository if possible. The
// `NotFound` error is thrown if: // `NotFound` error is thrown if:
// //
@ -125,11 +107,29 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
Ok(repo) => { Ok(repo) => {
info!( info!(
"Opened local repository <green>{}</>.", "Opened local repository <green>{}</>.",
&pc.config.local.display() &pc.config.repos.local.display()
); );
Ok(repo) 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)?, 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> { fn find_package_files(pc: &PathConfig) -> Vec<ResPathBuf> {
let mut seen = Vec::new(); let mut seen = Vec::new();
for (_, package) in &pc.config.packages { for (_, packages) in &pc.config.packages {
for path in &package.configs { for path in packages {
if let Ok(resolved) = path::resolve(path) { if let Ok(resolved) = path::resolve(path) {
seen.push(resolved); seen.push(resolved);
} }
@ -214,16 +214,11 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
// Syncing // Syncing
// ======================================== // ========================================
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> { /// Adds all files to our index.
// 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 /// Checks explicitly if any changes have been detected in the newly constructed
// environment. /// index. If not, return `None`.
let _local_branch = pull(&pc, &repo)?; pub fn index_add(repo: &Repository) -> Result<Option<Index>> {
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
let mut index = repo.index()?; let mut index = repo.index()?;
index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?; index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?;
let diff_stats = repo let diff_stats = repo
@ -240,11 +235,48 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
&& diff_stats.insertions() == 0 && diff_stats.insertions() == 0
&& diff_stats.deletions() == 0 && diff_stats.deletions() == 0
{ {
info!("Nothing to push. Have you run `homesync stage`?"); Ok(None)
return Ok(()); } else {
Ok(Some(index))
} }
}
let signature = get_signature(&pc)?; /// 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(());
}
};
// Retrieve the latest commit before writing to the object database. // Retrieve the latest commit before writing to the object database.
let parent_commit = get_commit(&repo)?; let parent_commit = get_commit(&repo)?;
let index_oid = index.write_tree()?; 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); info!("Writing index to tree `{}`.", index_oid);
// Commit our changes and push them to our remote. // 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( repo.commit(
Some(&refspec), Some(&refspec),
&signature, &signature,
@ -262,11 +295,13 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
&index_tree, &index_tree,
&[&parent_commit], &[&parent_commit],
)?; )?;
let mut remote = get_remote(&pc, &repo)?;
remote.connect(Direction::Push)?; remote.connect(Direction::Push)?;
remote.push(&[&format!("{r}:{r}", r = refspec)], None)?; remote.push(&[&format!("{r}:{r}", r = refspec)], None)?;
info!( info!(
"Pushed changes to remote `{}/{}`.", "Pushed changes to remote `{}`.",
&pc.config.remote.name, &pc.config.remote.branch pc.config.repos.remote.tracking_branch(),
); );
Ok(()) 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>> { pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'repo>> {
validate_repo(&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 // Establish our remote. If the remote already exists, re-configure it
// blindly to point to the appropriate url. Our results should now exist // blindly to point to the appropriate url. Our results should now exist
// in a branch called `remotes/origin/<branch>`. // in a branch called `remotes/origin/<branch>`.
// https://git-scm.com/book/it/v2/Git-Basics-Working-with-Remotes // 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)?; let mut remote = get_remote(&pc, &repo)?;
remote.fetch(&[&pc.config.remote.branch], None, None)?; let mut fetch_options = get_fetch_options(&pc)?;
let remote_branch_name = format!("{}/{}", &pc.config.remote.name, &pc.config.remote.branch); 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)?; 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: // 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). // available. These should be rebased relative to remote (our upstream).
// 2. Our repository has been initialized in an empty state. The branch we // 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. // 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())?; 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 local_ref = repo.reference_to_annotated_commit(local_branch.get())?;
let signature = get_signature(&pc)?; let signature = get_signature(&pc)?;
repo.rebase(Some(&local_ref), Some(&remote_ref), None, None)? 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) Ok(local_branch)
} else { } else {
let local_branch = 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); info!("Created new local branch from `{}`.", remote_branch_name);
Ok(local_branch) Ok(local_branch)
} }
@ -332,23 +372,27 @@ fn get_commit(repo: &Repository) -> Result<Commit> {
.map_err(|_| git2::Error::from_str("Couldn't find commit"))?) .map_err(|_| git2::Error::from_str("Couldn't find commit"))?)
} }
/// Create or retrieve the remote specified within our configuration. /// Construct callbacks to supply authentication information on fetch/clone/etc.
/// fn get_fetch_options(pc: &PathConfig) -> Result<FetchOptions> {
/// This method also configures the fetchspec for the remote, explicitly mapping let public_path = match &pc.config.ssh.public {
/// the remote branch against our local one. Some(p) => Some(path::resolve(p)?),
/// None => None,
/// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec };
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> { let private_path = path::resolve(&pc.config.ssh.private)?;
repo.remote_set_url(&pc.config.remote.name, &pc.config.remote.url.to_string())?;
repo.remote_add_fetch( let mut callbacks = RemoteCallbacks::new();
&pc.config.remote.name, callbacks.credentials(move |_url, username_from_url, _allowed_types| {
// We could go with "*" instead of {branch} for all remote branches. Cred::ssh_key(
&format!( username_from_url.unwrap(),
"+refs/heads/{branch}:refs/remotes/origin/{branch}", public_path.as_ref().map(|p| p.resolved().as_ref()),
branch = pc.config.remote.branch private_path.as_ref(),
), None,
)?; )
Ok(repo.find_remote(&pc.config.remote.name)?) });
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
Ok(fetch_options)
} }
/// Generate a new signature at the current time. /// Generate a new signature at the current time.