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 git2::{
|
||||
Branch, BranchType, Commit, IndexAddOption, ObjectType, Reference, Remote, Repository,
|
||||
Signature,
|
||||
Branch, BranchType, Commit, DiffOptions, Direction, IndexAddOption, ObjectType, Remote,
|
||||
Repository, Signature,
|
||||
};
|
||||
use path::ResPathBuf;
|
||||
use simplelog::{info, paris, warn};
|
||||
|
@ -61,7 +61,8 @@ impl fmt::Display for Error {
|
|||
Error::IOError(e) => write!(f, "{}", e),
|
||||
Error::InvalidBareRepo => write!(
|
||||
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),
|
||||
}
|
||||
|
@ -83,7 +84,7 @@ fn clone_or_init(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
|
|||
);
|
||||
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
|
||||
// repositories work.
|
||||
// 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<()> {
|
||||
let workdir = repo.workdir().ok_or(Error::InvalidBareRepo)?;
|
||||
let workdir = validate_repo(&repo)?;
|
||||
let repo_files = find_repo_files(&workdir)?;
|
||||
let package_files = find_package_files(&pc);
|
||||
|
||||
// Find all files in our repository that are no longer being referenced in
|
||||
// our primary config file. They should be removed from the repository.
|
||||
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 {
|
||||
let relative = repo_file
|
||||
.resolved()
|
||||
.strip_prefix(workdir)
|
||||
.strip_prefix(&workdir)
|
||||
.expect("Relative git file could not be stripped properly.")
|
||||
.to_path_buf();
|
||||
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
|
||||
// repository.
|
||||
for package_file in &package_files {
|
||||
|
@ -203,6 +206,7 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
|||
}
|
||||
fs::copy(package_file.resolved(), copy)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -211,65 +215,72 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
|||
// ========================================
|
||||
|
||||
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
|
||||
repo.workdir().ok_or(Error::InvalidBareRepo)?;
|
||||
// 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.
|
||||
// First pull to make sure there are no conflicts when we push our changes.
|
||||
// This will also perform validation and construct our local and remote
|
||||
// environment.
|
||||
let _local_branch = pull(&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
|
||||
// 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 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_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"),
|
||||
info!("Writing index to tree `{}`.", index_oid);
|
||||
|
||||
// Commit our changes and push them to our remote.
|
||||
let refspec = format!("refs/heads/{}", &pc.config.remote.branch);
|
||||
repo.commit(
|
||||
Some(&refspec),
|
||||
&signature,
|
||||
&signature,
|
||||
// TODO(jrpotter): See how many previous pushes were made.
|
||||
// TODO(jrpotter): Come up with a more useful message.
|
||||
"homesync push",
|
||||
&index_tree,
|
||||
// iter/collect to collect an array of references.
|
||||
&parent_commit.iter().collect::<Vec<_>>()[..],
|
||||
&[&parent_commit],
|
||||
)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn pull(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||
pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'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)?;
|
||||
remote.fetch(&[&pc.config.remote.branch], None, None)?;
|
||||
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).
|
||||
// 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.
|
||||
//
|
||||
// TODO(jrpotter): If changes are available, need to stage them and then
|
||||
// reapply.
|
||||
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) {
|
||||
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)?
|
||||
.finish(Some(&signature))?;
|
||||
info!("Rebased local branch onto `{}`.", remote_branch_name);
|
||||
Ok(local_branch)
|
||||
} else {
|
||||
let local_branch =
|
||||
repo.branch_from_annotated_commit(&pc.config.remote.branch, &remote_ref, false)?;
|
||||
info!("Created new local branch from `{}`.", remote_branch_name);
|
||||
Ok(local_branch)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 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
|
||||
/// apply to it.
|
||||
fn validate_repo(repo: &Repository) -> Result<()> {
|
||||
repo.workdir().ok_or(Error::InvalidBareRepo)?;
|
||||
Ok(())
|
||||
fn validate_repo(repo: &Repository) -> Result<PathBuf> {
|
||||
Ok(repo.workdir().ok_or(Error::InvalidBareRepo)?.to_path_buf())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -332,23 +351,7 @@ fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<
|
|||
Ok(repo.find_remote(&pc.config.remote.name)?)
|
||||
}
|
||||
|
||||
/// Finds the latest commit relative to HEAD.
|
||||
///
|
||||
/// You should probably switch branches (refer to `switch_branch`) before
|
||||
/// 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)?,
|
||||
},
|
||||
}
|
||||
/// 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)?)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue