diff --git a/asyncgit/src/sync/branch/merge_commit.rs b/asyncgit/src/sync/branch/merge_commit.rs new file mode 100644 index 00000000..072e1b36 --- /dev/null +++ b/asyncgit/src/sync/branch/merge_commit.rs @@ -0,0 +1,250 @@ +//! merging from upstream + +use super::BranchType; +use crate::{ + error::{Error, Result}, + sync::{utils, CommitId}, +}; +use git2::MergeOptions; +use scopetime::scope_time; + +/// merge upstream using a merge commit without conflicts. fails if not possible without conflicts +pub fn merge_upstream_commit( + repo_path: &str, + branch_name: &str, +) -> Result { + scope_time!("merge_upstream_commit"); + + let repo = utils::repo(repo_path)?; + + let branch = repo.find_branch(branch_name, BranchType::Local)?; + let upstream = branch.upstream()?; + + let upstream_commit = upstream.get().peel_to_commit()?; + + let annotated_upstream = + repo.find_annotated_commit(upstream_commit.id())?; + + let (analysis, _) = + repo.merge_analysis(&[&annotated_upstream])?; + + if !analysis.is_normal() { + return Err(Error::Generic( + "normal merge not possible".into(), + )); + } + + //TODO: support merge on unborn + if analysis.is_unborn() { + return Err(Error::Generic("head is unborn".into())); + } + + let mut opt = MergeOptions::default(); + opt.fail_on_conflict(true); + + repo.merge(&[&annotated_upstream], Some(&mut opt), None)?; + + assert!(!repo.index()?.has_conflicts()); + + let signature = + crate::sync::commit::signature_allow_undefined_name(&repo)?; + let mut index = repo.index()?; + let tree_id = index.write_tree()?; + let tree = repo.find_tree(tree_id)?; + + let head_commit = repo.find_commit( + crate::sync::utils::get_head_repo(&repo)?.into(), + )?; + let parents = vec![&head_commit, &upstream_commit]; + + //find remote url for this branch + let remote_url = { + let branch_refname = + branch.get().name().ok_or_else(|| { + Error::Generic(String::from( + "branch refname not found", + )) + })?; + let buf = repo.branch_upstream_remote(branch_refname)?; + let remote = + repo.find_remote(buf.as_str().ok_or_else(|| { + Error::Generic(String::from("remote name not found")) + })?)?; + remote.url().unwrap_or_default().to_string() + }; + + let commit_id = repo + .commit( + Some("HEAD"), + &signature, + &signature, + format!("Merge '{}' from {}", branch_name, remote_url) + .as_str(), + &tree, + parents.as_slice(), + )? + .into(); + + repo.cleanup_state()?; + + Ok(commit_id) +} + +#[cfg(test)] +mod test { + use super::super::merge_ff::test::write_commit_file; + use super::*; + use crate::sync::{ + branch_compare_upstream, + remotes::{fetch_origin, push::push}, + tests::{ + debug_cmd_print, get_commit_ids, repo_clone, + repo_init_bare, + }, + RepoState, + }; + + #[test] + fn test_merge_normal() { + let (r1_dir, _repo) = repo_init_bare().unwrap(); + + let (clone1_dir, clone1) = + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); + + let (clone2_dir, clone2) = + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); + + let clone2_dir = clone2_dir.path().to_str().unwrap(); + + // clone1 + + let commit1 = + write_commit_file(&clone1, "test.txt", "test", "commit1"); + + push( + clone1_dir.path().to_str().unwrap(), + "origin", + "master", + false, + None, + None, + ) + .unwrap(); + + // clone2 + + let commit2 = write_commit_file( + &clone2, + "test2.txt", + "test", + "commit2", + ); + + //push should fail since origin diverged + assert!(push( + clone2_dir, "origin", "master", false, None, None, + ) + .is_err()); + + //lets fetch from origin + let bytes = + fetch_origin(clone2_dir, "master", None, None).unwrap(); + assert!(bytes > 0); + + //we should be one commit behind + assert_eq!( + branch_compare_upstream(clone2_dir, "master") + .unwrap() + .behind, + 1 + ); + + let merge_commit = + merge_upstream_commit(clone2_dir, "master").unwrap(); + + let state = crate::sync::repo_state(clone2_dir).unwrap(); + + assert_eq!(state, RepoState::Clean); + + let commits = get_commit_ids(&clone2, 10); + assert_eq!(commits.len(), 3); + assert_eq!(commits[0], merge_commit); + assert_eq!(commits[1], commit2); + assert_eq!(commits[2], commit1); + + //verify commit msg + let details = + crate::sync::get_commit_details(clone2_dir, merge_commit) + .unwrap(); + assert_eq!( + details.message.unwrap().combine(), + format!( + "Merge 'master' from {}", + r1_dir.path().to_str().unwrap() + ) + ); + } + + #[test] + fn test_merge_normal_conflict() { + let (r1_dir, _repo) = repo_init_bare().unwrap(); + + let (clone1_dir, clone1) = + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); + + let (clone2_dir, clone2) = + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); + + // clone1 + + write_commit_file(&clone1, "test.bin", "test", "commit1"); + + debug_cmd_print( + clone2_dir.path().to_str().unwrap(), + "git status", + ); + + push( + clone1_dir.path().to_str().unwrap(), + "origin", + "master", + false, + None, + None, + ) + .unwrap(); + + // clone2 + + write_commit_file(&clone2, "test.bin", "foobar", "commit2"); + + let bytes = fetch_origin( + clone2_dir.path().to_str().unwrap(), + "master", + None, + None, + ) + .unwrap(); + assert!(bytes > 0); + + let res = merge_upstream_commit( + clone2_dir.path().to_str().unwrap(), + "master", + ); + + //this should have failed cause it would create a conflict + assert!(res.is_err()); + + let state = crate::sync::repo_state( + clone2_dir.path().to_str().unwrap(), + ) + .unwrap(); + + //make sure we left the repo not in some merging state + assert_eq!(state, RepoState::Clean); + + //check that we still only have the first commit + let commits = get_commit_ids(&clone1, 10); + assert_eq!(commits.len(), 1); + } +} diff --git a/asyncgit/src/sync/branch/merge.rs b/asyncgit/src/sync/branch/merge_ff.rs similarity index 97% rename from asyncgit/src/sync/branch/merge.rs rename to asyncgit/src/sync/branch/merge_ff.rs index b5354228..586c769c 100644 --- a/asyncgit/src/sync/branch/merge.rs +++ b/asyncgit/src/sync/branch/merge_ff.rs @@ -33,6 +33,7 @@ pub fn branch_merge_upstream_fastforward( )); } + //TODO: support merge on unborn if analysis.is_unborn() { return Err(Error::Generic("head is unborn".into())); } @@ -45,7 +46,7 @@ pub fn branch_merge_upstream_fastforward( } #[cfg(test)] -mod test { +pub mod test { use super::*; use crate::sync::{ commit, @@ -61,7 +62,7 @@ mod test { use std::{fs::File, io::Write, path::Path}; // write, stage and commit a file - fn write_commit_file( + pub fn write_commit_file( repo: &Repository, file: &str, content: &str, @@ -85,7 +86,7 @@ mod test { } #[test] - fn test_merge() { + fn test_merge_fastforward() { let (r1_dir, _repo) = repo_init_bare().unwrap(); let (clone1_dir, clone1) = diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index 5bf405a0..ac7d81ed 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -1,6 +1,7 @@ //! branch functions -pub mod merge; +pub mod merge_commit; +pub mod merge_ff; pub mod rename; use super::{ diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index 56457c72..40981cbb 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -33,7 +33,7 @@ pub fn amend( /// Wrap Repository::signature to allow unknown user.name. /// /// See . -fn signature_allow_undefined_name( +pub(crate) fn signature_allow_undefined_name( repo: &Repository, ) -> std::result::Result, git2::Error> { let signature = repo.signature(); diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index aeaf91d7..f43e0d6f 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -25,8 +25,9 @@ pub mod utils; pub use branch::{ branch_compare_upstream, checkout_branch, create_branch, delete_branch, get_branches_info, - merge::branch_merge_upstream_fastforward, rename::rename_branch, - BranchCompare, BranchInfo, + merge_commit::merge_upstream_commit, + merge_ff::branch_merge_upstream_fastforward, + rename::rename_branch, BranchCompare, BranchInfo, }; pub use commit::{amend, commit, tag}; pub use commit_details::{ diff --git a/src/components/pull.rs b/src/components/pull.rs index 1c943253..358a76a1 100644 --- a/src/components/pull.rs +++ b/src/components/pull.rs @@ -1,3 +1,4 @@ +use super::PushComponent; use crate::{ components::{ cred::CredComponent, visibility_blocking, CommandBlocking, @@ -5,7 +6,7 @@ use crate::{ }, keys::SharedKeyConfig, queue::{InternalEvent, Queue}, - strings, + strings, try_or_popup, ui::{self, style::SharedTheme}, }; use anyhow::Result; @@ -30,8 +31,6 @@ use tui::{ Frame, }; -use super::PushComponent; - /// pub struct PullComponent { visible: bool, @@ -158,11 +157,12 @@ impl PullComponent { &self.branch, ); if let Err(err) = merge_res { - self.queue.borrow_mut().push_back( - InternalEvent::ShowErrorMsg(format!( - "merge failed:\n{}", - err - )), + log::error!("ff merge failed: {}", err); + + try_or_popup!( + self, + "merge failed:", + sync::merge_upstream_commit(CWD, &self.branch) ); } }