Skip to content

Commit 1714fba

Browse files
author
Stephan Dilly
authored
support conflict-free merge-commit (#561)
* support conflict-free merge-commit
1 parent bead96d commit 1714fba

File tree

6 files changed

+268
-15
lines changed

6 files changed

+268
-15
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
//! merging from upstream
2+
3+
use super::BranchType;
4+
use crate::{
5+
error::{Error, Result},
6+
sync::{utils, CommitId},
7+
};
8+
use git2::MergeOptions;
9+
use scopetime::scope_time;
10+
11+
/// merge upstream using a merge commit without conflicts. fails if not possible without conflicts
12+
pub fn merge_upstream_commit(
13+
repo_path: &str,
14+
branch_name: &str,
15+
) -> Result<CommitId> {
16+
scope_time!("merge_upstream_commit");
17+
18+
let repo = utils::repo(repo_path)?;
19+
20+
let branch = repo.find_branch(branch_name, BranchType::Local)?;
21+
let upstream = branch.upstream()?;
22+
23+
let upstream_commit = upstream.get().peel_to_commit()?;
24+
25+
let annotated_upstream =
26+
repo.find_annotated_commit(upstream_commit.id())?;
27+
28+
let (analysis, _) =
29+
repo.merge_analysis(&[&annotated_upstream])?;
30+
31+
if !analysis.is_normal() {
32+
return Err(Error::Generic(
33+
"normal merge not possible".into(),
34+
));
35+
}
36+
37+
//TODO: support merge on unborn
38+
if analysis.is_unborn() {
39+
return Err(Error::Generic("head is unborn".into()));
40+
}
41+
42+
let mut opt = MergeOptions::default();
43+
opt.fail_on_conflict(true);
44+
45+
repo.merge(&[&annotated_upstream], Some(&mut opt), None)?;
46+
47+
assert!(!repo.index()?.has_conflicts());
48+
49+
let signature =
50+
crate::sync::commit::signature_allow_undefined_name(&repo)?;
51+
let mut index = repo.index()?;
52+
let tree_id = index.write_tree()?;
53+
let tree = repo.find_tree(tree_id)?;
54+
55+
let head_commit = repo.find_commit(
56+
crate::sync::utils::get_head_repo(&repo)?.into(),
57+
)?;
58+
let parents = vec![&head_commit, &upstream_commit];
59+
60+
//find remote url for this branch
61+
let remote_url = {
62+
let branch_refname =
63+
branch.get().name().ok_or_else(|| {
64+
Error::Generic(String::from(
65+
"branch refname not found",
66+
))
67+
})?;
68+
let buf = repo.branch_upstream_remote(branch_refname)?;
69+
let remote =
70+
repo.find_remote(buf.as_str().ok_or_else(|| {
71+
Error::Generic(String::from("remote name not found"))
72+
})?)?;
73+
remote.url().unwrap_or_default().to_string()
74+
};
75+
76+
let commit_id = repo
77+
.commit(
78+
Some("HEAD"),
79+
&signature,
80+
&signature,
81+
format!("Merge '{}' from {}", branch_name, remote_url)
82+
.as_str(),
83+
&tree,
84+
parents.as_slice(),
85+
)?
86+
.into();
87+
88+
repo.cleanup_state()?;
89+
90+
Ok(commit_id)
91+
}
92+
93+
#[cfg(test)]
94+
mod test {
95+
use super::super::merge_ff::test::write_commit_file;
96+
use super::*;
97+
use crate::sync::{
98+
branch_compare_upstream,
99+
remotes::{fetch_origin, push::push},
100+
tests::{
101+
debug_cmd_print, get_commit_ids, repo_clone,
102+
repo_init_bare,
103+
},
104+
RepoState,
105+
};
106+
107+
#[test]
108+
fn test_merge_normal() {
109+
let (r1_dir, _repo) = repo_init_bare().unwrap();
110+
111+
let (clone1_dir, clone1) =
112+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
113+
114+
let (clone2_dir, clone2) =
115+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
116+
117+
let clone2_dir = clone2_dir.path().to_str().unwrap();
118+
119+
// clone1
120+
121+
let commit1 =
122+
write_commit_file(&clone1, "test.txt", "test", "commit1");
123+
124+
push(
125+
clone1_dir.path().to_str().unwrap(),
126+
"origin",
127+
"master",
128+
false,
129+
None,
130+
None,
131+
)
132+
.unwrap();
133+
134+
// clone2
135+
136+
let commit2 = write_commit_file(
137+
&clone2,
138+
"test2.txt",
139+
"test",
140+
"commit2",
141+
);
142+
143+
//push should fail since origin diverged
144+
assert!(push(
145+
clone2_dir, "origin", "master", false, None, None,
146+
)
147+
.is_err());
148+
149+
//lets fetch from origin
150+
let bytes =
151+
fetch_origin(clone2_dir, "master", None, None).unwrap();
152+
assert!(bytes > 0);
153+
154+
//we should be one commit behind
155+
assert_eq!(
156+
branch_compare_upstream(clone2_dir, "master")
157+
.unwrap()
158+
.behind,
159+
1
160+
);
161+
162+
let merge_commit =
163+
merge_upstream_commit(clone2_dir, "master").unwrap();
164+
165+
let state = crate::sync::repo_state(clone2_dir).unwrap();
166+
167+
assert_eq!(state, RepoState::Clean);
168+
169+
let commits = get_commit_ids(&clone2, 10);
170+
assert_eq!(commits.len(), 3);
171+
assert_eq!(commits[0], merge_commit);
172+
assert_eq!(commits[1], commit2);
173+
assert_eq!(commits[2], commit1);
174+
175+
//verify commit msg
176+
let details =
177+
crate::sync::get_commit_details(clone2_dir, merge_commit)
178+
.unwrap();
179+
assert_eq!(
180+
details.message.unwrap().combine(),
181+
format!(
182+
"Merge 'master' from {}",
183+
r1_dir.path().to_str().unwrap()
184+
)
185+
);
186+
}
187+
188+
#[test]
189+
fn test_merge_normal_conflict() {
190+
let (r1_dir, _repo) = repo_init_bare().unwrap();
191+
192+
let (clone1_dir, clone1) =
193+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
194+
195+
let (clone2_dir, clone2) =
196+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
197+
198+
// clone1
199+
200+
write_commit_file(&clone1, "test.bin", "test", "commit1");
201+
202+
debug_cmd_print(
203+
clone2_dir.path().to_str().unwrap(),
204+
"git status",
205+
);
206+
207+
push(
208+
clone1_dir.path().to_str().unwrap(),
209+
"origin",
210+
"master",
211+
false,
212+
None,
213+
None,
214+
)
215+
.unwrap();
216+
217+
// clone2
218+
219+
write_commit_file(&clone2, "test.bin", "foobar", "commit2");
220+
221+
let bytes = fetch_origin(
222+
clone2_dir.path().to_str().unwrap(),
223+
"master",
224+
None,
225+
None,
226+
)
227+
.unwrap();
228+
assert!(bytes > 0);
229+
230+
let res = merge_upstream_commit(
231+
clone2_dir.path().to_str().unwrap(),
232+
"master",
233+
);
234+
235+
//this should have failed cause it would create a conflict
236+
assert!(res.is_err());
237+
238+
let state = crate::sync::repo_state(
239+
clone2_dir.path().to_str().unwrap(),
240+
)
241+
.unwrap();
242+
243+
//make sure we left the repo not in some merging state
244+
assert_eq!(state, RepoState::Clean);
245+
246+
//check that we still only have the first commit
247+
let commits = get_commit_ids(&clone1, 10);
248+
assert_eq!(commits.len(), 1);
249+
}
250+
}

asyncgit/src/sync/branch/merge.rs renamed to asyncgit/src/sync/branch/merge_ff.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub fn branch_merge_upstream_fastforward(
3333
));
3434
}
3535

36+
//TODO: support merge on unborn
3637
if analysis.is_unborn() {
3738
return Err(Error::Generic("head is unborn".into()));
3839
}
@@ -45,7 +46,7 @@ pub fn branch_merge_upstream_fastforward(
4546
}
4647

4748
#[cfg(test)]
48-
mod test {
49+
pub mod test {
4950
use super::*;
5051
use crate::sync::{
5152
commit,
@@ -61,7 +62,7 @@ mod test {
6162
use std::{fs::File, io::Write, path::Path};
6263

6364
// write, stage and commit a file
64-
fn write_commit_file(
65+
pub fn write_commit_file(
6566
repo: &Repository,
6667
file: &str,
6768
content: &str,
@@ -85,7 +86,7 @@ mod test {
8586
}
8687

8788
#[test]
88-
fn test_merge() {
89+
fn test_merge_fastforward() {
8990
let (r1_dir, _repo) = repo_init_bare().unwrap();
9091

9192
let (clone1_dir, clone1) =

asyncgit/src/sync/branch/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! branch functions
22
3-
pub mod merge;
3+
pub mod merge_commit;
4+
pub mod merge_ff;
45
pub mod rename;
56

67
use super::{

asyncgit/src/sync/commit.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub fn amend(
3333
/// Wrap Repository::signature to allow unknown user.name.
3434
///
3535
/// See <https://github.com/extrawurst/gitui/issues/79>.
36-
fn signature_allow_undefined_name(
36+
pub(crate) fn signature_allow_undefined_name(
3737
repo: &Repository,
3838
) -> std::result::Result<Signature<'_>, git2::Error> {
3939
let signature = repo.signature();

asyncgit/src/sync/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ pub mod utils;
2525
pub use branch::{
2626
branch_compare_upstream, checkout_branch, create_branch,
2727
delete_branch, get_branches_info,
28-
merge::branch_merge_upstream_fastforward, rename::rename_branch,
29-
BranchCompare, BranchInfo,
28+
merge_commit::merge_upstream_commit,
29+
merge_ff::branch_merge_upstream_fastforward,
30+
rename::rename_branch, BranchCompare, BranchInfo,
3031
};
3132
pub use commit::{amend, commit, tag};
3233
pub use commit_details::{

src/components/pull.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
use super::PushComponent;
12
use crate::{
23
components::{
34
cred::CredComponent, visibility_blocking, CommandBlocking,
45
CommandInfo, Component, DrawableComponent,
56
},
67
keys::SharedKeyConfig,
78
queue::{InternalEvent, Queue},
8-
strings,
9+
strings, try_or_popup,
910
ui::{self, style::SharedTheme},
1011
};
1112
use anyhow::Result;
@@ -30,8 +31,6 @@ use tui::{
3031
Frame,
3132
};
3233

33-
use super::PushComponent;
34-
3534
///
3635
pub struct PullComponent {
3736
visible: bool,
@@ -158,11 +157,12 @@ impl PullComponent {
158157
&self.branch,
159158
);
160159
if let Err(err) = merge_res {
161-
self.queue.borrow_mut().push_back(
162-
InternalEvent::ShowErrorMsg(format!(
163-
"merge failed:\n{}",
164-
err
165-
)),
160+
log::error!("ff merge failed: {}", err);
161+
162+
try_or_popup!(
163+
self,
164+
"merge failed:",
165+
sync::merge_upstream_commit(CWD, &self.branch)
166166
);
167167
}
168168
}

0 commit comments

Comments
 (0)