Fuller idea on how push can work.

pull/3/head
Joshua Potter 2022-01-07 09:09:43 -05:00
parent 53f0b399c0
commit 9fdbb34967
1 changed files with 80 additions and 77 deletions

View File

@ -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 {
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); 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)?,
},
}
} }