Allow specifying SSH keys and clean up config further.
parent
9fdbb34967
commit
014a7d72e5
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()])
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
192
src/git.rs
192
src/git.rs
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue