Skip to content

support rebase merge (conflict free only) #567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -524,8 +524,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