Skip to content

Commit

Permalink
support rebase merge (conflict free only) (#567)
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephan Dilly committed May 27, 2021
1 parent 7c87a59 commit 79a3f98
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `[s]` key repurposed to trigger line based (un)stage

### Added
- support pull via rebase (using config `pull.rebase`) ([#566](https://github.com/extrawurst/gitui/issues/566))
- support stage/unstage selected lines ([#59](https://github.com/extrawurst/gitui/issues/59))
- support discarding selected lines ([#59](https://github.com/extrawurst/gitui/issues/59))
- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568))
Expand Down
226 changes: 226 additions & 0 deletions asyncgit/src/sync/branch/merge_rebase.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
//! merging from upstream (rebase)

use crate::{
error::{Error, Result},
sync::utils,
};
use git2::BranchType;
use scopetime::scope_time;

/// trys merging current branch with its upstrema using rebase
pub fn merge_upstream_rebase(
repo_path: &str,
branch_name: &str,
) -> Result<()> {
scope_time!("merge_upstream_rebase");

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 branch_commit = branch.get().peel_to_commit()?;
let annotated_branch =
repo.find_annotated_commit(branch_commit.id())?;

let rebase = repo.rebase(
Some(&annotated_branch),
Some(&annotated_upstream),
None,
None,
)?;

let signature =
crate::sync::commit::signature_allow_undefined_name(&repo)?;

for e in rebase {
let _op = e?;
// dbg!(op.id());
// dbg!(op.kind());
}

let mut rebase = repo.open_rebase(None)?;

if repo.index()?.has_conflicts() {
rebase.abort()?;

Err(Error::Generic(String::from("conflicts while merging")))
} else {
rebase.commit(None, &signature, None)?;

rebase.finish(Some(&signature))?;

Ok(())
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::sync::{
branch_compare_upstream, get_commits_info,
remotes::{fetch_origin, push::push},
tests::{
debug_cmd_print, get_commit_ids, repo_clone,
repo_init_bare, write_commit_file,
},
RepoState,
};
use git2::Repository;

fn get_commit_msgs(r: &Repository) -> Vec<String> {
let commits = get_commit_ids(r, 10);
get_commits_info(
r.workdir().unwrap().to_str().unwrap(),
&commits,
10,
)
.unwrap()
.into_iter()
.map(|c| c.message)
.collect()
}

#[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 clone1_dir = clone1_dir.path().to_str().unwrap();

// clone1

let _commit1 =
write_commit_file(&clone1, "test.txt", "test", "commit1");

push(clone1_dir, "origin", "master", false, None, None)
.unwrap();

// clone2

let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();

let clone2_dir = clone2_dir.path().to_str().unwrap();

let _commit2 = write_commit_file(
&clone2,
"test2.txt",
"test",
"commit2",
);

push(clone2_dir, "origin", "master", false, None, None)
.unwrap();

// clone1

let _commit3 = write_commit_file(
&clone1,
"test3.txt",
"test",
"commit3",
);

//lets fetch from origin
let bytes =
fetch_origin(clone1_dir, "master", None, None).unwrap();
assert!(bytes > 0);

//we should be one commit behind
assert_eq!(
branch_compare_upstream(clone1_dir, "master")
.unwrap()
.behind,
1
);

// debug_cmd_print(clone1_dir, "git log");

merge_upstream_rebase(clone1_dir, "master").unwrap();

debug_cmd_print(clone1_dir, "git log");

let state = crate::sync::repo_state(clone1_dir).unwrap();

assert_eq!(state, RepoState::Clean);

let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![
String::from("commit3"),
String::from("commit2"),
String::from("commit1")
]
);
}

#[test]
fn test_merge_conflict() {
let (r1_dir, _repo) = repo_init_bare().unwrap();

let (clone1_dir, clone1) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();

let clone1_dir = clone1_dir.path().to_str().unwrap();

// clone1

let _commit1 =
write_commit_file(&clone1, "test.txt", "test", "commit1");

push(clone1_dir, "origin", "master", false, None, None)
.unwrap();

// clone2

let (clone2_dir, clone2) =
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();

let clone2_dir = clone2_dir.path().to_str().unwrap();

let _commit2 = write_commit_file(
&clone2,
"test2.txt",
"test",
"commit2",
);

push(clone2_dir, "origin", "master", false, None, None)
.unwrap();

// clone1

let _commit3 =
write_commit_file(&clone1, "test2.txt", "foo", "commit3");

let bytes =
fetch_origin(clone1_dir, "master", None, None).unwrap();
assert!(bytes > 0);

assert_eq!(
branch_compare_upstream(clone1_dir, "master")
.unwrap()
.behind,
1
);

let res = merge_upstream_rebase(clone1_dir, "master");
assert!(res.is_err());

let state = crate::sync::repo_state(clone1_dir).unwrap();

assert_eq!(state, RepoState::Clean);

let commits = get_commit_msgs(&clone1);
assert_eq!(
commits,
vec![String::from("commit3"), String::from("commit1")]
);
}
}
15 changes: 15 additions & 0 deletions asyncgit/src/sync/branch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

pub mod merge_commit;
pub mod merge_ff;
pub mod merge_rebase;
pub mod rename;

use super::{
Expand Down Expand Up @@ -108,6 +109,20 @@ pub(crate) fn branch_set_upstream(
Ok(())
}

/// returns whether the pull merge strategy is set to rebase
pub fn config_is_pull_rebase(repo_path: &str) -> Result<bool> {
let repo = utils::repo(repo_path)?;
let config = repo.config()?;

if let Ok(rebase) = config.get_entry("pull.rebase") {
let value =
rebase.value().map(String::from).unwrap_or_default();
return Ok(value == "true");
};

Ok(false)
}

///
pub fn branch_compare_upstream(
repo_path: &str,
Expand Down
7 changes: 4 additions & 3 deletions asyncgit/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ mod tags;
pub mod utils;

pub use branch::{
branch_compare_upstream, checkout_branch, create_branch,
delete_branch, get_branches_info,
branch_compare_upstream, checkout_branch, config_is_pull_rebase,
create_branch, delete_branch, get_branches_info,
merge_commit::merge_upstream_commit,
merge_ff::branch_merge_upstream_fastforward,
rename::rename_branch, BranchCompare, BranchInfo,
merge_rebase::merge_upstream_rebase, rename::rename_branch,
BranchCompare, BranchInfo,
};
pub use commit::{amend, commit, tag};
pub use commit_details::{
Expand Down
4 changes: 2 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,8 @@ impl App {
.queue
.borrow_mut()
.push_back(InternalEvent::Push(branch, force)),
Action::PullMerge(_) => {
self.pull_popup.try_conflict_free_merge();
Action::PullMerge { rebase, .. } => {
self.pull_popup.try_conflict_free_merge(rebase);
flags.insert(NeedsUpdate::ALL);
}
},
Expand Down
32 changes: 22 additions & 10 deletions src/components/pull.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ impl PullComponent {
let branch_compare =
sync::branch_compare_upstream(CWD, &self.branch)?;
if branch_compare.behind > 0 {
let merge_res = sync::branch_merge_upstream_fastforward(
let ff_res = sync::branch_merge_upstream_fastforward(
CWD,
&self.branch,
);
if let Err(err) = merge_res {
log::trace!("ff merge failed: {}", err);
if let Err(err) = ff_res {
log::trace!("ff failed: {}", err);
self.confirm_merge(branch_compare.behind);
}
}
Expand All @@ -166,17 +166,29 @@ impl PullComponent {
Ok(())
}

pub fn try_conflict_free_merge(&self) {
try_or_popup!(
self,
"merge failed:",
sync::merge_upstream_commit(CWD, &self.branch)
);
pub fn try_conflict_free_merge(&self, rebase: bool) {
if rebase {
try_or_popup!(
self,
"rebase failed:",
sync::merge_upstream_rebase(CWD, &self.branch)
);
} else {
try_or_popup!(
self,
"merge failed:",
sync::merge_upstream_commit(CWD, &self.branch)
);
}
}

fn confirm_merge(&mut self, incoming: usize) {
self.queue.borrow_mut().push_back(
InternalEvent::ConfirmAction(Action::PullMerge(incoming)),
InternalEvent::ConfirmAction(Action::PullMerge {
incoming,
rebase: sync::config_is_pull_rebase(CWD)
.unwrap_or_default(),
}),
);
self.hide();
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ impl ResetComponent {
branch.rsplit('/').next().expect("There was no / in the head reference which is impossible in git"),
),
),
Action::PullMerge(incoming) => (
strings::confirm_title_merge(&self.key_config),
strings::confirm_msg_merge(&self.key_config,*incoming),
Action::PullMerge{incoming,rebase} => (
strings::confirm_title_merge(&self.key_config,*rebase),
strings::confirm_msg_merge(&self.key_config,*incoming,*rebase),
),
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub enum Action {
StashDrop(CommitId),
DeleteBranch(String),
ForcePush(String, bool),
PullMerge(usize),
PullMerge { incoming: usize, rebase: bool },
}

///
Expand Down
18 changes: 15 additions & 3 deletions src/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,26 @@ pub fn confirm_title_stashdrop(
) -> String {
"Drop".to_string()
}
pub fn confirm_title_merge(_key_config: &SharedKeyConfig) -> String {
"Merge".to_string()
pub fn confirm_title_merge(
_key_config: &SharedKeyConfig,
rebase: bool,
) -> String {
if rebase {
"Merge (via rebase)".to_string()
} else {
"Merge (via commit)".to_string()
}
}
pub fn confirm_msg_merge(
_key_config: &SharedKeyConfig,
incoming: usize,
rebase: bool,
) -> String {
format!("confirm merge of {} incoming commits? ", incoming)
if rebase {
format!("Rebase onto {} incoming commits?", incoming)
} else {
format!("Merge of {} incoming commits?", incoming)
}
}
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
"confirm file reset?".to_string()
Expand Down

0 comments on commit 79a3f98

Please sign in to comment.