From 63085593c48616b568c901ed687047b2e7c0b61a Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Thu, 6 Jan 2022 08:54:46 -0500 Subject: [PATCH] Low level thoughts on how git pushing could work. --- examples/config.yaml | 5 +- src/config.rs | 92 ++++++++++++++++++++++++------------ src/git.rs | 108 +++++++++++++++++++++++++++++++------------ src/lib.rs | 6 +++ src/main.rs | 2 + 5 files changed, 152 insertions(+), 61 deletions(-) diff --git a/examples/config.yaml b/examples/config.yaml index 4ba8f21..1f9a06d 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,6 +1,9 @@ --- local: $HOME/.homesync -remote: "https://github.com/jrpotter/home-config.git" +remote: + name: origin + branch: master + url: "https://github.com/jrpotter/home-config.git" packages: alacritty: configs: diff --git a/src/config.rs b/src/config.rs index 2ad6380..12ba3ff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -82,10 +82,17 @@ pub struct Package { pub configs: Vec, } +#[derive(Debug, Deserialize, Serialize)] +pub struct Remote { + pub name: String, + pub branch: String, + pub url: Url, +} + #[derive(Debug, Deserialize, Serialize)] pub struct Config { pub local: PathBuf, - pub remote: Url, + pub remote: Remote, pub packages: BTreeMap, } @@ -161,41 +168,66 @@ pub fn reload(pc: &PathConfig) -> Result { // Creation // ======================================== -fn prompt_local(path: Option<&Path>) -> Result { - let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string()); - print!( - "Local git repository <{}> (enter to continue): ", - colorize_string(format!("{}", &default)), - ); +fn prompt_default(prompt: &str, default: String) -> Result { + print!("{}", prompt); io::stdout().flush()?; - let mut local = String::new(); - io::stdin().read_line(&mut local)?; - // Defer validation this path until initialization of the repository. - let local = local.trim(); - if local.is_empty() { - Ok(PathBuf::from(default)) + let mut value = String::new(); + io::stdin().read_line(&mut value)?; + let trimmed = value.trim(); + if trimmed.is_empty() { + Ok(default) } else { - Ok(PathBuf::from(local)) + Ok(trimmed.to_owned()) } } -fn prompt_remote(url: Option<&Url>) -> Result { - let default = url.map_or("https://github.com/owner/repo.git".to_owned(), |u| { - u.to_string() +fn prompt_local(path: Option<&Path>) -> Result { + let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string()); + let value = prompt_default( + &format!( + "Local git repository <{}> (enter to continue): ", + colorize_string(format!("{}", &default)), + ), + default, + )?; + Ok(PathBuf::from(value)) +} + +fn prompt_remote(remote: Option<&Remote>) -> Result { + let default_name = remote.map_or("origin".to_owned(), |r| r.name.to_owned()); + let remote_name = prompt_default( + &format!( + "Remote git name <{}> (enter to continue): ", + colorize_string(format!("{}", &default_name)) + ), + default_name, + )?; + + let default_branch = remote.map_or("origin".to_owned(), |r| r.branch.to_owned()); + let remote_branch = prompt_default( + &format!( + "Remote git branch <{}> (enter to continue): ", + colorize_string(format!("{}", &default_branch)) + ), + default_branch, + )?; + + let default_url = remote.map_or("https://github.com/owner/repo.git".to_owned(), |r| { + r.url.to_string() }); - print!( - "Remote git repository <{}> (enter to continue): ", - colorize_string(format!("{}", &default)), - ); - io::stdout().flush()?; - let mut remote = String::new(); - io::stdin().read_line(&mut remote)?; - let remote = remote.trim(); - if remote.is_empty() { - Ok(Url::parse(&default)?) - } else { - Ok(Url::parse(&remote)?) - } + let remote_url = prompt_default( + &format!( + "Remote git url <{}> (enter to continue): ", + colorize_string(format!("{}", &default_url)) + ), + default_url, + )?; + + Ok(Remote { + name: remote_name, + branch: remote_branch, + url: Url::parse(&remote_url)?, + }) } pub fn write(path: &ResPathBuf, loaded: Option) -> Result { diff --git a/src/git.rs b/src/git.rs index b11828f..1ff3652 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,5 +1,5 @@ use super::{config::PathConfig, path}; -use git2::Repository; +use git2::{IndexAddOption, ObjectType, Remote, Repository, Signature, StashFlags}; use path::ResPathBuf; use simplelog::{info, paris}; use std::{ @@ -10,35 +10,6 @@ use std::{ result, }; -// 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"), - // ======================================== // Error // ======================================== @@ -232,3 +203,80 @@ pub fn apply(pc: &PathConfig, repo: &Repository) -> Result<()> { } Ok(()) } + +// ======================================== +// Syncing +// ======================================== + +fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result> { + // Sets a new remote if it does not yet exist. + repo.remote_set_url(&pc.config.remote.name, &pc.config.remote.url.to_string())?; + // We could go with "*" instead of referencing the one branch, but let's be + // specific for the time being. + // https://git-scm.com/book/en/v2/Git-Internals-The-Refspec + repo.remote_add_fetch( + &pc.config.remote.name, + &format!( + "+refs/heads/{branch}:refs/remotes/origin/{branch}", + branch = pc.config.remote.branch + ), + )?; + Ok(repo.find_remote(&pc.config.remote.name)?) +} + +pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> { + repo.workdir().ok_or(Error::NotWorkingRepo)?; + // Switch to the new branch we want to work on. If the branch does not + // exist, `set_head` will point to an unborn branch. + // https://git-scm.com/docs/git-check-ref-format. + repo.set_head(&format!("refs/heads/{}", pc.config.remote.branch))?; + // 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/`. + // https://git-scm.com/book/it/v2/Git-Basics-Working-with-Remotes + // TODO(jrpotter): Rebase against the remote. + let mut remote = get_remote(&pc, &repo)?; + remote.fetch(&[&pc.config.remote.branch], None, None)?; + // Find the latest commit on our current branch. This could be empty if just + // having initialized the repository. + let parent_commit = match repo.head() { + Ok(head) => { + let obj = head + .resolve()? + .peel(ObjectType::Commit)? + .into_commit() + .map_err(|_| git2::Error::from_str("Couldn't find commit"))?; + vec![obj] + } + // An unborn branch error is fired when first initializing the + // repository. Our first commit will create the branch. + Err(e) => match e.code() { + git2::ErrorCode::UnbornBranch => vec![], + _ => Err(e)?, + }, + }; + // 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 `. + // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects + let mut index = repo.index()?; + index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?; + let index_oid = index.write_tree()?; + let index_tree = repo.find_tree(index_oid)?; + // Stash any of our changes. We will first fetch from the remote and then + // apply our changes on top of it. + // TODO(jrpotter): Add user and email to config. Remove init comamnd. + // TODO(jrpotter): Cannot stash changes with no initial commit. + let signature = Signature::now("homesync", "robot@homesync.org")?; + let commit_oid = repo.commit( + Some("HEAD"), + &signature, + &signature, + // TODO(jrpotter): See how many previous pushes were made. + "homesync push", + &index_tree, + // iter/collect to collect an array of references. + &parent_commit.iter().collect::>()[..], + )?; + let _commit = repo.find_commit(commit_oid)?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 00f2d32..edf1c98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,3 +54,9 @@ pub fn run_list(config: PathConfig) -> Result { config::list_packages(config); Ok(()) } + +pub fn run_push(config: PathConfig) -> Result { + let mut repo = git::init(&config)?; + git::push(&config, &mut repo)?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 8280814..a681863 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,7 @@ fn main() { ) .subcommand(App::new("init").about("Initialize the homesync local repository")) .subcommand(App::new("list").about("See which packages homesync manages")) + .subcommand(App::new("push").about("Push changes from local to remote")) .get_matches(); if let Err(e) = dispatch(matches) { @@ -95,6 +96,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box> { Ok(()) } Some(("list", _)) => Ok(homesync::run_list(config)?), + Some(("push", _)) => Ok(homesync::run_push(config)?), _ => unreachable!(), } }