Fuller idea on how push can work.
parent
53f0b399c0
commit
9fdbb34967
155
src/git.rs
155
src/git.rs
|
@ -1,7 +1,7 @@
|
||||||
use super::{config::PathConfig, path};
|
use super::{config::PathConfig, path};
|
||||||
use git2::{
|
use git2::{
|
||||||
Branch, BranchType, Commit, IndexAddOption, ObjectType, Reference, Remote, Repository,
|
Branch, BranchType, Commit, DiffOptions, Direction, IndexAddOption, ObjectType, Remote,
|
||||||
Signature,
|
Repository, Signature,
|
||||||
};
|
};
|
||||||
use path::ResPathBuf;
|
use path::ResPathBuf;
|
||||||
use simplelog::{info, paris, warn};
|
use simplelog::{info, paris, warn};
|
||||||
|
@ -61,7 +61,8 @@ impl fmt::Display for Error {
|
||||||
Error::IOError(e) => write!(f, "{}", e),
|
Error::IOError(e) => write!(f, "{}", e),
|
||||||
Error::InvalidBareRepo => write!(
|
Error::InvalidBareRepo => write!(
|
||||||
f,
|
f,
|
||||||
"Local repository should be a working directory. Did you manually initialize with `--bare`?"
|
"Local repository should be a working directory. Did you manually initialize with \
|
||||||
|
`--bare`?"
|
||||||
),
|
),
|
||||||
Error::VarError(e) => write!(f, "{}", e),
|
Error::VarError(e) => write!(f, "{}", e),
|
||||||
}
|
}
|
||||||
|
@ -83,7 +84,7 @@ fn clone_or_init(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
|
||||||
);
|
);
|
||||||
Ok(repo)
|
Ok(repo)
|
||||||
}
|
}
|
||||||
Err(e) if (e.code() == git2::ErrorCode::NotFound || e.code() == git2::ErrorCode::Auth) => {
|
Err(e) if e.code() == git2::ErrorCode::NotFound || e.code() == git2::ErrorCode::Auth => {
|
||||||
// TODO(jrpotter): Setup authentication callbacks so private
|
// TODO(jrpotter): Setup authentication callbacks so private
|
||||||
// repositories work.
|
// repositories work.
|
||||||
// https://docs.rs/git2/0.13.25/git2/build/struct.RepoBuilder.html#example
|
// https://docs.rs/git2/0.13.25/git2/build/struct.RepoBuilder.html#example
|
||||||
|
@ -169,9 +170,10 @@ fn find_package_files(pc: &PathConfig) -> Vec<ResPathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
let workdir = repo.workdir().ok_or(Error::InvalidBareRepo)?;
|
let workdir = validate_repo(&repo)?;
|
||||||
let repo_files = find_repo_files(&workdir)?;
|
let repo_files = find_repo_files(&workdir)?;
|
||||||
let package_files = find_package_files(&pc);
|
let package_files = find_package_files(&pc);
|
||||||
|
|
||||||
// Find all files in our repository that are no longer being referenced in
|
// Find all files in our repository that are no longer being referenced in
|
||||||
// our primary config file. They should be removed from the repository.
|
// our primary config file. They should be removed from the repository.
|
||||||
let lookup_files: HashSet<PathBuf> = package_files
|
let lookup_files: HashSet<PathBuf> = package_files
|
||||||
|
@ -181,7 +183,7 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
for repo_file in &repo_files {
|
for repo_file in &repo_files {
|
||||||
let relative = repo_file
|
let relative = repo_file
|
||||||
.resolved()
|
.resolved()
|
||||||
.strip_prefix(workdir)
|
.strip_prefix(&workdir)
|
||||||
.expect("Relative git file could not be stripped properly.")
|
.expect("Relative git file could not be stripped properly.")
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
if !lookup_files.contains(&relative) {
|
if !lookup_files.contains(&relative) {
|
||||||
|
@ -193,6 +195,7 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all resolvable files in our primary config and copy them into the
|
// Find all resolvable files in our primary config and copy them into the
|
||||||
// repository.
|
// repository.
|
||||||
for package_file in &package_files {
|
for package_file in &package_files {
|
||||||
|
@ -203,6 +206,7 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
}
|
}
|
||||||
fs::copy(package_file.resolved(), copy)?;
|
fs::copy(package_file.resolved(), copy)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,65 +215,72 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
|
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
|
||||||
repo.workdir().ok_or(Error::InvalidBareRepo)?;
|
// First pull to make sure there are no conflicts when we push our changes.
|
||||||
// Switch to the new branch we want to work on. If the branch does not
|
// This will also perform validation and construct our local and remote
|
||||||
// exist, `set_head` will point to an unborn branch.
|
// environment.
|
||||||
// https://git-scm.com/docs/git-check-ref-format.
|
let _local_branch = pull(&pc, &repo)?;
|
||||||
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)?;
|
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
|
// 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>`.
|
// 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
|
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||||
let mut index = repo.index()?;
|
let mut index = repo.index()?;
|
||||||
index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?;
|
index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?;
|
||||||
|
let diff_stats = repo
|
||||||
|
.diff_index_to_workdir(
|
||||||
|
Some(&index),
|
||||||
|
Some(
|
||||||
|
DiffOptions::new()
|
||||||
|
.include_untracked(true)
|
||||||
|
.include_unreadable(true),
|
||||||
|
),
|
||||||
|
)?
|
||||||
|
.stats()?;
|
||||||
|
if diff_stats.files_changed() == 0
|
||||||
|
&& diff_stats.insertions() == 0
|
||||||
|
&& diff_stats.deletions() == 0
|
||||||
|
{
|
||||||
|
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()?;
|
let index_oid = index.write_tree()?;
|
||||||
let index_tree = repo.find_tree(index_oid)?;
|
let index_tree = repo.find_tree(index_oid)?;
|
||||||
// Stash any of our changes. We will first fetch from the remote and then
|
info!("Writing index to tree `{}`.", index_oid);
|
||||||
// apply our changes on top of it.
|
|
||||||
// TODO(jrpotter): Add user and email to config. Remove init comamnd.
|
// Commit our changes and push them to our remote.
|
||||||
// TODO(jrpotter): Cannot stash changes with no initial commit.
|
let refspec = format!("refs/heads/{}", &pc.config.remote.branch);
|
||||||
let signature = Signature::now("homesync", "robot@homesync.org")?;
|
repo.commit(
|
||||||
let commit_oid = repo.commit(
|
Some(&refspec),
|
||||||
Some("HEAD"),
|
|
||||||
&signature,
|
&signature,
|
||||||
&signature,
|
&signature,
|
||||||
// TODO(jrpotter): See how many previous pushes were made.
|
// TODO(jrpotter): Come up with a more useful message.
|
||||||
"homesync push",
|
"homesync push",
|
||||||
&index_tree,
|
&index_tree,
|
||||||
// iter/collect to collect an array of references.
|
&[&parent_commit],
|
||||||
&parent_commit.iter().collect::<Vec<_>>()[..],
|
|
||||||
)?;
|
)?;
|
||||||
let _commit = repo.find_commit(commit_oid)?;
|
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
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pull(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'repo>> {
|
||||||
validate_repo(&repo)?;
|
validate_repo(&repo)?;
|
||||||
|
|
||||||
|
// 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)?;
|
let mut remote = get_remote(&pc, &repo)?;
|
||||||
remote.fetch(&[&pc.config.remote.branch], None, None)?;
|
remote.fetch(&[&pc.config.remote.branch], None, None)?;
|
||||||
let remote_branch_name = format!("{}/{}", &pc.config.remote.name, &pc.config.remote.branch);
|
let remote_branch_name = format!("{}/{}", &pc.config.remote.name, &pc.config.remote.branch);
|
||||||
|
@ -282,6 +293,9 @@ pub fn pull(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
// available. These should be rebased relative to remote (our upstream).
|
// available. These should be rebased relative to remote (our upstream).
|
||||||
// 2. Our repository has been initialized in an empty state. The branch we
|
// 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.
|
// 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())?;
|
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.remote.branch, BranchType::Local) {
|
||||||
let local_ref = repo.reference_to_annotated_commit(local_branch.get())?;
|
let local_ref = repo.reference_to_annotated_commit(local_branch.get())?;
|
||||||
|
@ -289,28 +303,33 @@ pub fn pull(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
repo.rebase(Some(&local_ref), Some(&remote_ref), None, None)?
|
repo.rebase(Some(&local_ref), Some(&remote_ref), None, None)?
|
||||||
.finish(Some(&signature))?;
|
.finish(Some(&signature))?;
|
||||||
info!("Rebased local branch onto `{}`.", remote_branch_name);
|
info!("Rebased local branch onto `{}`.", remote_branch_name);
|
||||||
|
Ok(local_branch)
|
||||||
} else {
|
} else {
|
||||||
|
let local_branch =
|
||||||
repo.branch_from_annotated_commit(&pc.config.remote.branch, &remote_ref, false)?;
|
repo.branch_from_annotated_commit(&pc.config.remote.branch, &remote_ref, false)?;
|
||||||
info!("Created new local branch from `{}`.", remote_branch_name);
|
info!("Created new local branch from `{}`.", remote_branch_name);
|
||||||
|
Ok(local_branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Utility
|
// Utility
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
/// Generate a new signature at the current time.
|
|
||||||
fn get_signature(pc: &PathConfig) -> Result<Signature> {
|
|
||||||
Ok(Signature::now(&pc.config.user.name, &pc.config.user.email)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the repository we are working in supports the operations we want to
|
/// Verify the repository we are working in supports the operations we want to
|
||||||
/// apply to it.
|
/// apply to it.
|
||||||
fn validate_repo(repo: &Repository) -> Result<()> {
|
fn validate_repo(repo: &Repository) -> Result<PathBuf> {
|
||||||
repo.workdir().ok_or(Error::InvalidBareRepo)?;
|
Ok(repo.workdir().ok_or(Error::InvalidBareRepo)?.to_path_buf())
|
||||||
Ok(())
|
}
|
||||||
|
|
||||||
|
/// Return the latest commit off of HEAD.
|
||||||
|
fn get_commit(repo: &Repository) -> Result<Commit> {
|
||||||
|
Ok(repo
|
||||||
|
.head()?
|
||||||
|
.resolve()?
|
||||||
|
.peel(ObjectType::Commit)?
|
||||||
|
.into_commit()
|
||||||
|
.map_err(|_| git2::Error::from_str("Couldn't find commit"))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create or retrieve the remote specified within our configuration.
|
/// Create or retrieve the remote specified within our configuration.
|
||||||
|
@ -332,23 +351,7 @@ fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<
|
||||||
Ok(repo.find_remote(&pc.config.remote.name)?)
|
Ok(repo.find_remote(&pc.config.remote.name)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the latest commit relative to HEAD.
|
/// Generate a new signature at the current time.
|
||||||
///
|
fn get_signature(pc: &PathConfig) -> Result<Signature> {
|
||||||
/// You should probably switch branches (refer to `switch_branch`) before
|
Ok(Signature::now(&pc.config.user.name, &pc.config.user.email)?)
|
||||||
/// calling this function.
|
|
||||||
fn get_head_commit(repo: &Repository) -> Result<Option<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"))?;
|
|
||||||
Ok(Some(obj))
|
|
||||||
}
|
|
||||||
Err(e) => match e.code() {
|
|
||||||
git2::ErrorCode::UnbornBranch => Ok(None),
|
|
||||||
_ => Err(e)?,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue