Low level thoughts on how git pushing could work.
parent
90065a4ffe
commit
63085593c4
|
@ -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:
|
||||
|
|
|
@ -82,10 +82,17 @@ pub struct Package {
|
|||
pub configs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[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<String, Package>,
|
||||
}
|
||||
|
||||
|
@ -161,41 +168,66 @@ pub fn reload(pc: &PathConfig) -> Result<PathConfig> {
|
|||
// Creation
|
||||
// ========================================
|
||||
|
||||
fn prompt_local(path: Option<&Path>) -> Result<PathBuf> {
|
||||
let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string());
|
||||
print!(
|
||||
"Local git repository <{}> (enter to continue): ",
|
||||
colorize_string(format!("<yellow>{}</>", &default)),
|
||||
);
|
||||
fn prompt_default(prompt: &str, default: String) -> Result<String> {
|
||||
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<Url> {
|
||||
let default = url.map_or("https://github.com/owner/repo.git".to_owned(), |u| {
|
||||
u.to_string()
|
||||
fn prompt_local(path: Option<&Path>) -> Result<PathBuf> {
|
||||
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!("<yellow>{}</>", &default)),
|
||||
),
|
||||
default,
|
||||
)?;
|
||||
Ok(PathBuf::from(value))
|
||||
}
|
||||
|
||||
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()
|
||||
});
|
||||
print!(
|
||||
"Remote git repository <{}> (enter to continue): ",
|
||||
colorize_string(format!("<yellow>{}</>", &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!("<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> {
|
||||
|
|
108
src/git.rs
108
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<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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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<dyn Error>> {
|
|||
Ok(())
|
||||
}
|
||||
Some(("list", _)) => Ok(homesync::run_list(config)?),
|
||||
Some(("push", _)) => Ok(homesync::run_push(config)?),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue