Low level thoughts on how git pushing could work.

pull/3/head
Joshua Potter 2022-01-06 08:54:46 -05:00
parent 90065a4ffe
commit 63085593c4
5 changed files with 152 additions and 61 deletions

View File

@ -1,6 +1,9 @@
--- ---
local: $HOME/.homesync 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: packages:
alacritty: alacritty:
configs: configs:

View File

@ -82,10 +82,17 @@ pub struct Package {
pub configs: Vec<PathBuf>, pub configs: Vec<PathBuf>,
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct Remote {
pub name: String,
pub branch: String,
pub url: Url,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Config { pub struct Config {
pub local: PathBuf, pub local: PathBuf,
pub remote: Url, pub remote: Remote,
pub packages: BTreeMap<String, Package>, pub packages: BTreeMap<String, Package>,
} }
@ -161,41 +168,66 @@ pub fn reload(pc: &PathConfig) -> Result<PathConfig> {
// Creation // Creation
// ======================================== // ========================================
fn prompt_local(path: Option<&Path>) -> Result<PathBuf> { fn prompt_default(prompt: &str, default: String) -> Result<String> {
let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string()); print!("{}", prompt);
print!(
"Local git repository <{}> (enter to continue): ",
colorize_string(format!("<yellow>{}</>", &default)),
);
io::stdout().flush()?; io::stdout().flush()?;
let mut local = String::new(); let mut value = String::new();
io::stdin().read_line(&mut local)?; io::stdin().read_line(&mut value)?;
// Defer validation this path until initialization of the repository. let trimmed = value.trim();
let local = local.trim(); if trimmed.is_empty() {
if local.is_empty() { Ok(default)
Ok(PathBuf::from(default))
} else { } else {
Ok(PathBuf::from(local)) Ok(trimmed.to_owned())
} }
} }
fn prompt_remote(url: Option<&Url>) -> Result<Url> { fn prompt_local(path: Option<&Path>) -> Result<PathBuf> {
let default = url.map_or("https://github.com/owner/repo.git".to_owned(), |u| { let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string());
u.to_string() let value = prompt_default(
}); &format!(
print!( "Local git repository <{}> (enter to continue): ",
"Remote git repository <{}> (enter to continue): ",
colorize_string(format!("<yellow>{}</>", &default)), colorize_string(format!("<yellow>{}</>", &default)),
); ),
io::stdout().flush()?; default,
let mut remote = String::new(); )?;
io::stdin().read_line(&mut remote)?; Ok(PathBuf::from(value))
let remote = remote.trim();
if remote.is_empty() {
Ok(Url::parse(&default)?)
} else {
Ok(Url::parse(&remote)?)
} }
fn prompt_remote(remote: Option<&Remote>) -> Result<Remote> {
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!("<yellow>{}</>", &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!("<yellow>{}</>", &default_branch))
),
default_branch,
)?;
let default_url = remote.map_or("https://github.com/owner/repo.git".to_owned(), |r| {
r.url.to_string()
});
let remote_url = prompt_default(
&format!(
"Remote git url <{}> (enter to continue): ",
colorize_string(format!("<yellow>{}</>", &default_url))
),
default_url,
)?;
Ok(Remote {
name: remote_name,
branch: remote_branch,
url: Url::parse(&remote_url)?,
})
} }
pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> { pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {

View File

@ -1,5 +1,5 @@
use super::{config::PathConfig, path}; use super::{config::PathConfig, path};
use git2::Repository; use git2::{IndexAddOption, ObjectType, Remote, Repository, Signature, StashFlags};
use path::ResPathBuf; use path::ResPathBuf;
use simplelog::{info, paris}; use simplelog::{info, paris};
use std::{ use std::{
@ -10,35 +10,6 @@ use std::{
result, 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 // Error
// ======================================== // ========================================
@ -232,3 +203,80 @@ pub fn apply(pc: &PathConfig, repo: &Repository) -> Result<()> {
} }
Ok(()) Ok(())
} }
// ========================================
// Syncing
// ========================================
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
// 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/<branch>`.
// 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 <oid>`.
// 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::<Vec<_>>()[..],
)?;
let _commit = repo.find_commit(commit_oid)?;
Ok(())
}

View File

@ -54,3 +54,9 @@ pub fn run_list(config: PathConfig) -> Result {
config::list_packages(config); config::list_packages(config);
Ok(()) Ok(())
} }
pub fn run_push(config: PathConfig) -> Result {
let mut repo = git::init(&config)?;
git::push(&config, &mut repo)?;
Ok(())
}

View File

@ -62,6 +62,7 @@ fn main() {
) )
.subcommand(App::new("init").about("Initialize the homesync local repository")) .subcommand(App::new("init").about("Initialize the homesync local repository"))
.subcommand(App::new("list").about("See which packages homesync manages")) .subcommand(App::new("list").about("See which packages homesync manages"))
.subcommand(App::new("push").about("Push changes from local to remote"))
.get_matches(); .get_matches();
if let Err(e) = dispatch(matches) { if let Err(e) = dispatch(matches) {
@ -95,6 +96,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
Some(("list", _)) => Ok(homesync::run_list(config)?), Some(("list", _)) => Ok(homesync::run_list(config)?),
Some(("push", _)) => Ok(homesync::run_push(config)?),
_ => unreachable!(), _ => unreachable!(),
} }
} }