diff --git a/src/git.rs b/src/git.rs index 0a24651..dff5ec4 100644 --- a/src/git.rs +++ b/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 { ); 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 { } 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 = 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/`. - // 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 `. // 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::>()[..], + &[&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> { 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/`. + // 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 { - repo.branch_from_annotated_commit(&pc.config.remote.branch, &remote_ref, false)?; + 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 { - 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 { + Ok(repo.workdir().ok_or(Error::InvalidBareRepo)?.to_path_buf()) +} + +/// Return the latest commit off of HEAD. +fn get_commit(repo: &Repository) -> Result { + 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 Result> { - 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 { + Ok(Signature::now(&pc.config.user.name, &pc.config.user.email)?) }