mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 00:48:35 +00:00
support conflict-free merge-commit (#561)
* support conflict-free merge-commit
This commit is contained in:
parent
bead96db08
commit
1714fbad49
6 changed files with 268 additions and 15 deletions
250
asyncgit/src/sync/branch/merge_commit.rs
Normal file
250
asyncgit/src/sync/branch/merge_commit.rs
Normal file
|
|
@ -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<CommitId> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) =
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! branch functions
|
||||
|
||||
pub mod merge;
|
||||
pub mod merge_commit;
|
||||
pub mod merge_ff;
|
||||
pub mod rename;
|
||||
|
||||
use super::{
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub fn amend(
|
|||
/// Wrap Repository::signature to allow unknown user.name.
|
||||
///
|
||||
/// See <https://github.com/extrawurst/gitui/issues/79>.
|
||||
fn signature_allow_undefined_name(
|
||||
pub(crate) fn signature_allow_undefined_name(
|
||||
repo: &Repository,
|
||||
) -> std::result::Result<Signature<'_>, git2::Error> {
|
||||
let signature = repo.signature();
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue