diff --git a/src/page.rs b/src/page.rs index c6b4046..d2a1ee0 100644 --- a/src/page.rs +++ b/src/page.rs @@ -14,11 +14,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + use git2::{ build::CheckoutBuilder, Branch, BranchType, Direction, ObjectType, Oid, Remote, Repository, }; #[cfg(not(test))] use log::info; +use std::io::{self, Write}; #[cfg(test)] use std::println as info; @@ -43,39 +45,99 @@ impl Page { fn create_repo(&self) -> ServiceResult { let repo = self.open_repo(); - if let Ok(repo) = repo { - return Ok(repo); + let repo = if let Ok(repo) = repo { + repo } else { info!("Cloning repository {} at {}", self.repo, self.path); Repository::clone(&self.repo, &self.path)?; + Repository::open(&self.path)? }; - let repo = Repository::open(&self.path)?; - self._fetch_upstream(&repo, &self.branch)?; - self.deploy_branch(&repo)?; + self._fetch_remote_branch(&repo, &self.branch)?; + self.deploy_branch(&repo).unwrap(); + Ok(repo) } - fn find_branch<'a>(&self, repo: &'a Repository) -> ServiceResult> { - let branch = repo.find_branch(&self.branch, BranchType::Local)?; - Ok(branch) - } - pub fn deploy_branch(&self, repo: &Repository) -> ServiceResult<()> { - let branch = self.find_branch(repo)?; + let mut checkout_options = CheckoutBuilder::default(); + checkout_options + .allow_conflicts(true) + .conflict_style_merge(true) + .force(); - let mut checkout_options = CheckoutBuilder::new(); - checkout_options.force(); + let refname = format!("refs/heads/{}", self.branch); - let tree = branch.get().peel(ObjectType::Tree)?; + repo.set_head(&refname).unwrap(); + repo.checkout_head(Some(&mut checkout_options)).unwrap(); - repo.checkout_tree(&tree, Some(&mut checkout_options))?; - repo.set_head(&format!("refs/heads/{}", self.branch))?; info!("Deploying branch {}", self.branch); Ok(()) } - fn _fetch_upstream(&self, repo: &Repository, branch: &str) -> ServiceResult<()> { + fn fetch<'a>(&self, repo: &'a git2::Repository) -> ServiceResult> { + let mut remote = repo.find_remote("origin")?; + log::info!("Fetching {} for repo", remote.name().unwrap()); + remote.fetch(&[&self.branch], None, None)?; + let fetch_head = repo.find_reference("FETCH_HEAD")?; + Ok(repo.reference_to_annotated_commit(&fetch_head)?) + } + + fn merge<'a>( + &self, + repo: &'a Repository, + fetch_commit: git2::AnnotatedCommit<'a>, + ) -> ServiceResult<()> { + // 1. do a merge analysis + let analysis = repo.merge_analysis(&[&fetch_commit])?; + + // 2. Do the appropriate merge + if analysis.0.is_fast_forward() { + //log::debug!("Doing a fast forward"); + log::debug!("Doing a fast forward"); + // do a fast forward + let refname = format!("refs/heads/{}", &self.branch); + match repo.find_reference(&refname) { + Ok(mut r) => { + log::debug!("fast forwarding"); + Self::fast_forward(repo, &mut r, &fetch_commit).unwrap(); + } + Err(_) => { + // The branch doesn't exist so just set the reference to the + // commit directly. Usually this is because you are pulling + // into an empty repository. + log::error!("Error in find ref"); + repo.reference( + &refname, + fetch_commit.id(), + true, + &format!("Setting {} to {}", &self.branch, fetch_commit.id()), + ) + .unwrap(); + repo.set_head(&refname).unwrap(); + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + .allow_conflicts(true) + .conflict_style_merge(true) + .force(), + )) + .unwrap(); + } + }; + } else if analysis.0.is_normal() { + // do a normal merge + // expects repo.head to point to the branch when is going to receive merges + let head_commit = repo + .reference_to_annotated_commit(&repo.head().unwrap()) + .unwrap(); + Self::normal_merge(&repo, &head_commit, &fetch_commit).unwrap(); + } else { + log::info!("Nothing to do..."); + } + Ok(()) + } + + fn _fetch_remote_branch(&self, repo: &Repository, branch: &str) -> ServiceResult<()> { let mut remote = Self::get_deploy_remote(repo)?; remote.connect(Direction::Fetch)?; info!("Updating repository {}", self.repo); @@ -84,21 +146,80 @@ impl Page { remote.disconnect()?; let branch = repo.find_branch(&remote_branch_name, BranchType::Remote)?; let commit = branch.get().peel_to_commit()?; - if self.find_branch(repo).is_err() { + if repo.find_branch(&self.branch, BranchType::Local).is_err() { repo.branch(&self.branch, &commit, true)?; } Ok(()) } + fn normal_merge( + repo: &Repository, + local: &git2::AnnotatedCommit, + remote: &git2::AnnotatedCommit, + ) -> Result<(), git2::Error> { + let local_tree = repo.find_commit(local.id())?.tree().unwrap(); + let remote_tree = repo.find_commit(remote.id())?.tree().unwrap(); + println!("{} {}", local.id(), remote.id()); + let ancestor = repo + .find_commit(repo.merge_base(local.id(), remote.id()).unwrap()) + .unwrap() + .tree() + .unwrap(); + let mut idx = repo + .merge_trees(&ancestor, &local_tree, &remote_tree, None) + .unwrap(); + + if idx.has_conflicts() { + log::debug!("Merge conflicts detected..."); + repo.checkout_index(Some(&mut idx), None)?; + return Ok(()); + } + let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; + // now create the merge commit + let msg = format!("Merge: {} into {}", remote.id(), local.id()); + let sig = repo.signature()?; + let local_commit = repo.find_commit(local.id())?; + let remote_commit = repo.find_commit(remote.id())?; + // Do our merge commit and set current branch head to that commit. + let _merge_commit = repo.commit( + Some("HEAD"), + &sig, + &sig, + &msg, + &result_tree, + &[&local_commit, &remote_commit], + )?; + // Set working tree to match head. + repo.checkout_head(None)?; + Ok(()) + } + + fn fast_forward( + repo: &Repository, + lb: &mut git2::Reference, + rc: &git2::AnnotatedCommit, + ) -> ServiceResult<()> { + let name = match lb.name() { + Some(s) => s.to_string(), + None => String::from_utf8_lossy(lb.name_bytes()).to_string(), + }; + let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id()); + log::debug!("{}", msg); + lb.set_target(rc.id(), &msg)?; + repo.set_head(&name)?; + repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; + Ok(()) + } + pub fn update(&self) -> ServiceResult<()> { let repo = self.create_repo()?; - self._fetch_upstream(&repo, &self.branch)?; - self.deploy_branch(&repo)?; + let fetch_commit = self.fetch(&repo)?; + self.merge(&repo, fetch_commit)?; Ok(()) } pub fn get_deploy_branch(&self, repo: &Repository) -> ServiceResult { - let branch = self.find_branch(repo)?; + let branch = repo.find_branch(&self.branch, BranchType::Local)?; if branch.is_head() { Ok(self.branch.clone()) } else {