Low level thoughts on how git pushing could work.
parent
90065a4ffe
commit
63085593c4
|
@ -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:
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
108
src/git.rs
108
src/git.rs
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue