Skip to content

Commit 79a3f98

Browse files
author
Stephan Dilly
committed
support rebase merge (conflict free only) (#567)
1 parent 7c87a59 commit 79a3f98

File tree

9 files changed

+289
-22
lines changed

9 files changed

+289
-22
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- `[s]` key repurposed to trigger line based (un)stage
1212

1313
### Added
14+
- support pull via rebase (using config `pull.rebase`) ([#566](https://github.com/extrawurst/gitui/issues/566))
1415
- support stage/unstage selected lines ([#59](https://github.com/extrawurst/gitui/issues/59))
1516
- support discarding selected lines ([#59](https://github.com/extrawurst/gitui/issues/59))
1617
- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568))
+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//! merging from upstream (rebase)
2+
3+
use crate::{
4+
error::{Error, Result},
5+
sync::utils,
6+
};
7+
use git2::BranchType;
8+
use scopetime::scope_time;
9+
10+
/// trys merging current branch with its upstrema using rebase
11+
pub fn merge_upstream_rebase(
12+
repo_path: &str,
13+
branch_name: &str,
14+
) -> Result<()> {
15+
scope_time!("merge_upstream_rebase");
16+
17+
let repo = utils::repo(repo_path)?;
18+
let branch = repo.find_branch(branch_name, BranchType::Local)?;
19+
let upstream = branch.upstream()?;
20+
let upstream_commit = upstream.get().peel_to_commit()?;
21+
let annotated_upstream =
22+
repo.find_annotated_commit(upstream_commit.id())?;
23+
24+
let branch_commit = branch.get().peel_to_commit()?;
25+
let annotated_branch =
26+
repo.find_annotated_commit(branch_commit.id())?;
27+
28+
let rebase = repo.rebase(
29+
Some(&annotated_branch),
30+
Some(&annotated_upstream),
31+
None,
32+
None,
33+
)?;
34+
35+
let signature =
36+
crate::sync::commit::signature_allow_undefined_name(&repo)?;
37+
38+
for e in rebase {
39+
let _op = e?;
40+
// dbg!(op.id());
41+
// dbg!(op.kind());
42+
}
43+
44+
let mut rebase = repo.open_rebase(None)?;
45+
46+
if repo.index()?.has_conflicts() {
47+
rebase.abort()?;
48+
49+
Err(Error::Generic(String::from("conflicts while merging")))
50+
} else {
51+
rebase.commit(None, &signature, None)?;
52+
53+
rebase.finish(Some(&signature))?;
54+
55+
Ok(())
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod test {
61+
use super::*;
62+
use crate::sync::{
63+
branch_compare_upstream, get_commits_info,
64+
remotes::{fetch_origin, push::push},
65+
tests::{
66+
debug_cmd_print, get_commit_ids, repo_clone,
67+
repo_init_bare, write_commit_file,
68+
},
69+
RepoState,
70+
};
71+
use git2::Repository;
72+
73+
fn get_commit_msgs(r: &Repository) -> Vec<String> {
74+
let commits = get_commit_ids(r, 10);
75+
get_commits_info(
76+
r.workdir().unwrap().to_str().unwrap(),
77+
&commits,
78+
10,
79+
)
80+
.unwrap()
81+
.into_iter()
82+
.map(|c| c.message)
83+
.collect()
84+
}
85+
86+
#[test]
87+
fn test_merge_normal() {
88+
let (r1_dir, _repo) = repo_init_bare().unwrap();
89+
90+
let (clone1_dir, clone1) =
91+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
92+
93+
let clone1_dir = clone1_dir.path().to_str().unwrap();
94+
95+
// clone1
96+
97+
let _commit1 =
98+
write_commit_file(&clone1, "test.txt", "test", "commit1");
99+
100+
push(clone1_dir, "origin", "master", false, None, None)
101+
.unwrap();
102+
103+
// clone2
104+
105+
let (clone2_dir, clone2) =
106+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
107+
108+
let clone2_dir = clone2_dir.path().to_str().unwrap();
109+
110+
let _commit2 = write_commit_file(
111+
&clone2,
112+
"test2.txt",
113+
"test",
114+
"commit2",
115+
);
116+
117+
push(clone2_dir, "origin", "master", false, None, None)
118+
.unwrap();
119+
120+
// clone1
121+
122+
let _commit3 = write_commit_file(
123+
&clone1,
124+
"test3.txt",
125+
"test",
126+
"commit3",
127+
);
128+
129+
//lets fetch from origin
130+
let bytes =
131+
fetch_origin(clone1_dir, "master", None, None).unwrap();
132+
assert!(bytes > 0);
133+
134+
//we should be one commit behind
135+
assert_eq!(
136+
branch_compare_upstream(clone1_dir, "master")
137+
.unwrap()
138+
.behind,
139+
1
140+
);
141+
142+
// debug_cmd_print(clone1_dir, "git log");
143+
144+
merge_upstream_rebase(clone1_dir, "master").unwrap();
145+
146+
debug_cmd_print(clone1_dir, "git log");
147+
148+
let state = crate::sync::repo_state(clone1_dir).unwrap();
149+
150+
assert_eq!(state, RepoState::Clean);
151+
152+
let commits = get_commit_msgs(&clone1);
153+
assert_eq!(
154+
commits,
155+
vec![
156+
String::from("commit3"),
157+
String::from("commit2"),
158+
String::from("commit1")
159+
]
160+
);
161+
}
162+
163+
#[test]
164+
fn test_merge_conflict() {
165+
let (r1_dir, _repo) = repo_init_bare().unwrap();
166+
167+
let (clone1_dir, clone1) =
168+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
169+
170+
let clone1_dir = clone1_dir.path().to_str().unwrap();
171+
172+
// clone1
173+
174+
let _commit1 =
175+
write_commit_file(&clone1, "test.txt", "test", "commit1");
176+
177+
push(clone1_dir, "origin", "master", false, None, None)
178+
.unwrap();
179+
180+
// clone2
181+
182+
let (clone2_dir, clone2) =
183+
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
184+
185+
let clone2_dir = clone2_dir.path().to_str().unwrap();
186+
187+
let _commit2 = write_commit_file(
188+
&clone2,
189+
"test2.txt",
190+
"test",
191+
"commit2",
192+
);
193+
194+
push(clone2_dir, "origin", "master", false, None, None)
195+
.unwrap();
196+
197+
// clone1
198+
199+
let _commit3 =
200+
write_commit_file(&clone1, "test2.txt", "foo", "commit3");
201+
202+
let bytes =
203+
fetch_origin(clone1_dir, "master", None, None).unwrap();
204+
assert!(bytes > 0);
205+
206+
assert_eq!(
207+
branch_compare_upstream(clone1_dir, "master")
208+
.unwrap()
209+
.behind,
210+
1
211+
);
212+
213+
let res = merge_upstream_rebase(clone1_dir, "master");
214+
assert!(res.is_err());
215+
216+
let state = crate::sync::repo_state(clone1_dir).unwrap();
217+
218+
assert_eq!(state, RepoState::Clean);
219+
220+
let commits = get_commit_msgs(&clone1);
221+
assert_eq!(
222+
commits,
223+
vec![String::from("commit3"), String::from("commit1")]
224+
);
225+
}
226+
}

asyncgit/src/sync/branch/mod.rs

+15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod merge_commit;
44
pub mod merge_ff;
5+
pub mod merge_rebase;
56
pub mod rename;
67

78
use super::{
@@ -108,6 +109,20 @@ pub(crate) fn branch_set_upstream(
108109
Ok(())
109110
}
110111

112+
/// returns whether the pull merge strategy is set to rebase
113+
pub fn config_is_pull_rebase(repo_path: &str) -> Result<bool> {
114+
let repo = utils::repo(repo_path)?;
115+
let config = repo.config()?;
116+
117+
if let Ok(rebase) = config.get_entry("pull.rebase") {
118+
let value =
119+
rebase.value().map(String::from).unwrap_or_default();
120+
return Ok(value == "true");
121+
};
122+
123+
Ok(false)
124+
}
125+
111126
///
112127
pub fn branch_compare_upstream(
113128
repo_path: &str,

asyncgit/src/sync/mod.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ mod tags;
2525
pub mod utils;
2626

2727
pub use branch::{
28-
branch_compare_upstream, checkout_branch, create_branch,
29-
delete_branch, get_branches_info,
28+
branch_compare_upstream, checkout_branch, config_is_pull_rebase,
29+
create_branch, delete_branch, get_branches_info,
3030
merge_commit::merge_upstream_commit,
3131
merge_ff::branch_merge_upstream_fastforward,
32-
rename::rename_branch, BranchCompare, BranchInfo,
32+
merge_rebase::merge_upstream_rebase, rename::rename_branch,
33+
BranchCompare, BranchInfo,
3334
};
3435
pub use commit::{amend, commit, tag};
3536
pub use commit_details::{

src/app.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -543,8 +543,8 @@ impl App {
543543
.queue
544544
.borrow_mut()
545545
.push_back(InternalEvent::Push(branch, force)),
546-
Action::PullMerge(_) => {
547-
self.pull_popup.try_conflict_free_merge();
546+
Action::PullMerge { rebase, .. } => {
547+
self.pull_popup.try_conflict_free_merge(rebase);
548548
flags.insert(NeedsUpdate::ALL);
549549
}
550550
},

src/components/pull.rs

+22-10
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,12 @@ impl PullComponent {
151151
let branch_compare =
152152
sync::branch_compare_upstream(CWD, &self.branch)?;
153153
if branch_compare.behind > 0 {
154-
let merge_res = sync::branch_merge_upstream_fastforward(
154+
let ff_res = sync::branch_merge_upstream_fastforward(
155155
CWD,
156156
&self.branch,
157157
);
158-
if let Err(err) = merge_res {
159-
log::trace!("ff merge failed: {}", err);
158+
if let Err(err) = ff_res {
159+
log::trace!("ff failed: {}", err);
160160
self.confirm_merge(branch_compare.behind);
161161
}
162162
}
@@ -166,17 +166,29 @@ impl PullComponent {
166166
Ok(())
167167
}
168168

169-
pub fn try_conflict_free_merge(&self) {
170-
try_or_popup!(
171-
self,
172-
"merge failed:",
173-
sync::merge_upstream_commit(CWD, &self.branch)
174-
);
169+
pub fn try_conflict_free_merge(&self, rebase: bool) {
170+
if rebase {
171+
try_or_popup!(
172+
self,
173+
"rebase failed:",
174+
sync::merge_upstream_rebase(CWD, &self.branch)
175+
);
176+
} else {
177+
try_or_popup!(
178+
self,
179+
"merge failed:",
180+
sync::merge_upstream_commit(CWD, &self.branch)
181+
);
182+
}
175183
}
176184

177185
fn confirm_merge(&mut self, incoming: usize) {
178186
self.queue.borrow_mut().push_back(
179-
InternalEvent::ConfirmAction(Action::PullMerge(incoming)),
187+
InternalEvent::ConfirmAction(Action::PullMerge {
188+
incoming,
189+
rebase: sync::config_is_pull_rebase(CWD)
190+
.unwrap_or_default(),
191+
}),
180192
);
181193
self.hide();
182194
}

src/components/reset.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ impl ResetComponent {
173173
branch.rsplit('/').next().expect("There was no / in the head reference which is impossible in git"),
174174
),
175175
),
176-
Action::PullMerge(incoming) => (
177-
strings::confirm_title_merge(&self.key_config),
178-
strings::confirm_msg_merge(&self.key_config,*incoming),
176+
Action::PullMerge{incoming,rebase} => (
177+
strings::confirm_title_merge(&self.key_config,*rebase),
178+
strings::confirm_msg_merge(&self.key_config,*incoming,*rebase),
179179
),
180180
};
181181
}

src/queue.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub enum Action {
3131
StashDrop(CommitId),
3232
DeleteBranch(String),
3333
ForcePush(String, bool),
34-
PullMerge(usize),
34+
PullMerge { incoming: usize, rebase: bool },
3535
}
3636

3737
///

src/strings.rs

+15-3
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,26 @@ pub fn confirm_title_stashdrop(
8989
) -> String {
9090
"Drop".to_string()
9191
}
92-
pub fn confirm_title_merge(_key_config: &SharedKeyConfig) -> String {
93-
"Merge".to_string()
92+
pub fn confirm_title_merge(
93+
_key_config: &SharedKeyConfig,
94+
rebase: bool,
95+
) -> String {
96+
if rebase {
97+
"Merge (via rebase)".to_string()
98+
} else {
99+
"Merge (via commit)".to_string()
100+
}
94101
}
95102
pub fn confirm_msg_merge(
96103
_key_config: &SharedKeyConfig,
97104
incoming: usize,
105+
rebase: bool,
98106
) -> String {
99-
format!("confirm merge of {} incoming commits? ", incoming)
107+
if rebase {
108+
format!("Rebase onto {} incoming commits?", incoming)
109+
} else {
110+
format!("Merge of {} incoming commits?", incoming)
111+
}
100112
}
101113
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
102114
"confirm file reset?".to_string()

0 commit comments

Comments
 (0)