mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-02 07:53:55 +03:00
feat: add support for merging or rebasing out-of-date branch before applying changes
This commit is contained in:
parent
9e9d2b5ecc
commit
448fcea28e
@ -183,7 +183,7 @@ pub fn apply_branch(
|
|||||||
|
|
||||||
let writer = branch::Writer::new(gb_repository);
|
let writer = branch::Writer::new(gb_repository);
|
||||||
|
|
||||||
let mut apply_branch = match branch::Reader::new(¤t_session_reader).read(branch_id) {
|
let mut branch = match branch::Reader::new(¤t_session_reader).read(branch_id) {
|
||||||
Ok(branch) => Ok(branch),
|
Ok(branch) => Ok(branch),
|
||||||
Err(reader::Error::NotFound) => Err(errors::ApplyBranchError::BranchNotFound(
|
Err(reader::Error::NotFound) => Err(errors::ApplyBranchError::BranchNotFound(
|
||||||
errors::BranchNotFoundError {
|
errors::BranchNotFoundError {
|
||||||
@ -194,21 +194,28 @@ pub fn apply_branch(
|
|||||||
Err(error) => Err(errors::ApplyBranchError::Other(error.into())),
|
Err(error) => Err(errors::ApplyBranchError::Other(error.into())),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
if branch.applied {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let target_commit = repo
|
let target_commit = repo
|
||||||
.find_commit(default_target.sha)
|
.find_commit(default_target.sha)
|
||||||
.context("failed to find target commit")?;
|
.context("failed to find target commit")?;
|
||||||
let target_tree = target_commit.tree().context("failed to get target tree")?;
|
let target_tree = target_commit.tree().context("failed to get target tree")?;
|
||||||
|
|
||||||
let mut branch_tree = repo
|
let branch_tree = repo
|
||||||
.find_tree(apply_branch.tree)
|
.find_tree(branch.tree)
|
||||||
.context("failed to find branch tree")?;
|
.context("failed to find branch tree")?;
|
||||||
|
|
||||||
// calculate the merge base and make sure it's the same as the target commit
|
// calculate the merge base and make sure it's the same as the target commit
|
||||||
// if not, we need to merge or rebase the branch to get it up to date
|
// if not, we need to merge or rebase the branch to get it up to date
|
||||||
|
|
||||||
let merge_base = repo
|
let merge_base = repo
|
||||||
.merge_base(default_target.sha, apply_branch.head)
|
.merge_base(default_target.sha, branch.head)
|
||||||
.context("failed to calculate merge base")?;
|
.context(format!(
|
||||||
|
"failed to find merge base between {} and {}",
|
||||||
|
default_target.sha, branch.head
|
||||||
|
))?;
|
||||||
if merge_base != default_target.sha {
|
if merge_base != default_target.sha {
|
||||||
// Branch is out of date, merge or rebase it
|
// Branch is out of date, merge or rebase it
|
||||||
let merge_base_tree = repo
|
let merge_base_tree = repo
|
||||||
@ -216,17 +223,27 @@ pub fn apply_branch(
|
|||||||
.context(format!("failed to find merge base commit {}", merge_base))?
|
.context(format!("failed to find merge base commit {}", merge_base))?
|
||||||
.tree()
|
.tree()
|
||||||
.context("failed to find merge base tree")?;
|
.context("failed to find merge base tree")?;
|
||||||
|
|
||||||
let mut merge_index = repo
|
let mut merge_index = repo
|
||||||
.merge_trees(&merge_base_tree, &branch_tree, &target_tree)
|
.merge_trees(&merge_base_tree, &branch_tree, &target_tree)
|
||||||
.context("failed to merge trees")?;
|
.context("failed to merge trees")?;
|
||||||
|
|
||||||
if merge_index.has_conflicts() {
|
if merge_index.has_conflicts() {
|
||||||
// currently we can only deal with the merge problem branch
|
// currently we can only deal with the merge problem branch
|
||||||
unapply_all_branches(gb_repository, project_repository)?;
|
for branch in super::get_status_by_branch(gb_repository, project_repository)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(branch, _)| branch)
|
||||||
|
.filter(|branch| branch.applied)
|
||||||
|
{
|
||||||
|
writer.write(&branch::Branch {
|
||||||
|
applied: false,
|
||||||
|
..branch
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
// apply the branch
|
// apply the branch
|
||||||
apply_branch.applied = true;
|
branch.applied = true;
|
||||||
writer.write(&apply_branch)?;
|
writer.write(&branch)?;
|
||||||
|
|
||||||
// checkout the conflicts
|
// checkout the conflicts
|
||||||
repo.checkout_index(&mut merge_index)
|
repo.checkout_index(&mut merge_index)
|
||||||
@ -258,31 +275,116 @@ pub fn apply_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let head_commit = repo
|
let head_commit = repo
|
||||||
.find_commit(apply_branch.head)
|
.find_commit(branch.head)
|
||||||
.context("failed to find head commit")?;
|
.context("failed to find head commit")?;
|
||||||
|
|
||||||
// commit our new upstream merge
|
let merged_branch_tree_oid = merge_index
|
||||||
let message = "merge upstream";
|
|
||||||
// write the merge commit
|
|
||||||
let branch_tree_oid = merge_index
|
|
||||||
.write_tree_to(repo)
|
.write_tree_to(repo)
|
||||||
.context("failed to write tree")?;
|
.context("failed to write tree")?;
|
||||||
branch_tree = repo
|
|
||||||
.find_tree(branch_tree_oid)
|
let merged_branch_tree = repo
|
||||||
|
.find_tree(merged_branch_tree_oid)
|
||||||
.context("failed to find tree")?;
|
.context("failed to find tree")?;
|
||||||
|
|
||||||
|
if branch.upstream.is_some() {
|
||||||
|
// branch was pushed to upstream. create a merge commit to avoid need
|
||||||
|
// of force pushing.
|
||||||
|
// TODO: make this configurable
|
||||||
|
|
||||||
let new_branch_head = project_repository.commit(
|
let new_branch_head = project_repository.commit(
|
||||||
user,
|
user,
|
||||||
message,
|
format!(
|
||||||
&branch_tree,
|
"Merged {}/{} into {}",
|
||||||
|
default_target.branch.remote(),
|
||||||
|
default_target.branch.branch(),
|
||||||
|
branch.name
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
&merged_branch_tree,
|
||||||
&[&head_commit, &target_commit],
|
&[&head_commit, &target_commit],
|
||||||
signing_key,
|
signing_key,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// ok, update the virtual branch
|
// ok, update the virtual branch
|
||||||
apply_branch.head = new_branch_head;
|
branch.head = new_branch_head;
|
||||||
apply_branch.tree = branch_tree_oid;
|
branch.tree = merged_branch_tree_oid;
|
||||||
writer.write(&apply_branch)?;
|
writer.write(&branch)?;
|
||||||
|
} else {
|
||||||
|
// branch was not pushed to upstream yet. attempt a rebase,
|
||||||
|
let (_, committer) = project_repository.git_signatures(user)?;
|
||||||
|
let annotated_branch_head = repo
|
||||||
|
.find_annotated_commit(branch.head)
|
||||||
|
.context("failed to find annotated branch head commit")?;
|
||||||
|
let annotated_upstream_base = repo
|
||||||
|
.find_annotated_commit(target_commit.id())
|
||||||
|
.context("failed to find annotated target commit")?;
|
||||||
|
let mut rebase_options = git2::RebaseOptions::new();
|
||||||
|
rebase_options.quiet(true);
|
||||||
|
rebase_options.inmemory(true);
|
||||||
|
let mut rebase = repo
|
||||||
|
.rebase(
|
||||||
|
Some(&annotated_branch_head),
|
||||||
|
Some(&annotated_upstream_base),
|
||||||
|
None,
|
||||||
|
Some(&mut rebase_options),
|
||||||
|
)
|
||||||
|
.context("failed to rebase")?;
|
||||||
|
|
||||||
|
let mut rebase_success = true;
|
||||||
|
// check to see if these commits have already been pushed
|
||||||
|
let mut last_rebase_head = branch.head;
|
||||||
|
while rebase.next().is_some() {
|
||||||
|
let index = rebase
|
||||||
|
.inmemory_index()
|
||||||
|
.context("failed to get inmemory index")?;
|
||||||
|
if index.has_conflicts() {
|
||||||
|
rebase_success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(commit_id) = rebase.commit(None, &committer.clone().into(), None) {
|
||||||
|
last_rebase_head = commit_id.into();
|
||||||
|
} else {
|
||||||
|
rebase_success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rebase_success {
|
||||||
|
// rebase worked out, rewrite the branch head
|
||||||
|
rebase.finish(None).context("failed to finish rebase")?;
|
||||||
|
branch.head = last_rebase_head;
|
||||||
|
branch.tree = merged_branch_tree_oid;
|
||||||
|
} else {
|
||||||
|
// rebase failed, do a merge commit
|
||||||
|
rebase.abort().context("failed to abort rebase")?;
|
||||||
|
|
||||||
|
// get tree from merge_tree_oid
|
||||||
|
let merge_tree = repo
|
||||||
|
.find_tree(merged_branch_tree_oid)
|
||||||
|
.context("failed to find tree")?;
|
||||||
|
|
||||||
|
// commit the merge tree oid
|
||||||
|
let new_branch_head = project_repository
|
||||||
|
.commit(
|
||||||
|
user,
|
||||||
|
format!(
|
||||||
|
"Merged {}/{} into {}",
|
||||||
|
default_target.branch.remote(),
|
||||||
|
default_target.branch.branch(),
|
||||||
|
branch.name
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
&merge_tree,
|
||||||
|
&[&head_commit, &target_commit],
|
||||||
|
signing_key,
|
||||||
|
)
|
||||||
|
.context("failed to commit merge")?;
|
||||||
|
|
||||||
|
branch.head = new_branch_head;
|
||||||
|
branch.tree = merged_branch_tree_oid;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let wd_tree = project_repository.get_wd_tree()?;
|
let wd_tree = project_repository.get_wd_tree()?;
|
||||||
@ -297,8 +399,8 @@ pub fn apply_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply the branch
|
// apply the branch
|
||||||
apply_branch.applied = true;
|
branch.applied = true;
|
||||||
writer.write(&apply_branch)?;
|
writer.write(&branch)?;
|
||||||
|
|
||||||
// checkout the merge index
|
// checkout the merge index
|
||||||
repo.checkout_index(&mut merge_index)
|
repo.checkout_index(&mut merge_index)
|
||||||
@ -550,39 +652,6 @@ pub fn unapply_branch(
|
|||||||
Ok(Some(target_branch))
|
Ok(Some(target_branch))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unapply_all_branches(
|
|
||||||
gb_repository: &gb_repository::Repository,
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
) -> Result<()> {
|
|
||||||
let applied_virtual_branches = list_applied_vbranches(gb_repository)?;
|
|
||||||
|
|
||||||
for branch in applied_virtual_branches {
|
|
||||||
let branch_id = branch.id;
|
|
||||||
unapply_branch(gb_repository, project_repository, &branch_id)
|
|
||||||
.context("failed to unapply branch")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_applied_vbranches(
|
|
||||||
gb_repository: &gb_repository::Repository,
|
|
||||||
) -> Result<Vec<Branch>, anyhow::Error> {
|
|
||||||
let current_session = gb_repository
|
|
||||||
.get_or_create_current_session()
|
|
||||||
.context("failed to get or create currnt session")?;
|
|
||||||
let current_session_reader = sessions::Reader::open(gb_repository, ¤t_session)
|
|
||||||
.context("failed to open current session")?;
|
|
||||||
let applied_virtual_branches = Iterator::new(¤t_session_reader)
|
|
||||||
.context("failed to create branch iterator")?
|
|
||||||
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
|
|
||||||
.context("failed to read virtual branches")?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|branch| branch.applied)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
Ok(applied_virtual_branches)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_base_tree<'a>(
|
fn find_base_tree<'a>(
|
||||||
repo: &'a git::Repository,
|
repo: &'a git::Repository,
|
||||||
branch_commit: &'a git::Commit<'a>,
|
branch_commit: &'a git::Commit<'a>,
|
||||||
|
Loading…
Reference in New Issue
Block a user