mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-30 01:17:37 +03:00
simplify update branch logic
This commit is contained in:
parent
20782cec64
commit
c513de478c
@ -242,247 +242,221 @@ pub fn update_base_branch(
|
||||
target.sha
|
||||
))?;
|
||||
|
||||
// ok, now we need to deal with a number of situations
|
||||
// 1. applied branch, uncommitted conflicts
|
||||
// 2. applied branch, committed conflicts but not uncommitted
|
||||
// 3. applied branch, no conflicts
|
||||
// 4. unapplied branch, uncommitted conflicts
|
||||
// 5. unapplied branch, committed conflicts but not uncommitted
|
||||
// 6. unapplied branch, no conflicts
|
||||
|
||||
let branch_writer = branch::Writer::new(gb_repository);
|
||||
|
||||
// try to update every branch
|
||||
let updated_vbranches = super::get_status_by_branch(gb_repository, project_repository)?
|
||||
.into_iter()
|
||||
.map(
|
||||
|(mut branch, all_files): (branch::Branch, super::BranchStatus)| -> Result<Option<branch::Branch>> {
|
||||
let branch_tree = if branch.applied {
|
||||
super::write_tree(project_repository, &target, &all_files).and_then(|tree_id| {
|
||||
repo.find_tree(tree_id)
|
||||
.context(format!("failed to find writen tree {}", tree_id))
|
||||
})?
|
||||
} else {
|
||||
repo.find_tree(branch.tree)
|
||||
.context(format!("failed to find tree for branch {}", branch.id))?
|
||||
.map(|(branch, _)| branch)
|
||||
.map(|branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
||||
let branch_tree = repo.find_tree(branch.tree)?;
|
||||
|
||||
let mut branch_merge_index = repo
|
||||
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree)
|
||||
.context(format!("failed to merge trees for branch {}", branch.id))?;
|
||||
|
||||
if branch_merge_index.has_conflicts() {
|
||||
// branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back.
|
||||
let branch = branch::Branch {
|
||||
applied: false,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
// try to merge the branch tree with the new target tree
|
||||
let mut branch_merge_index = repo
|
||||
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree)
|
||||
.context(format!("failed to merge trees for branch {}", branch.id))?;
|
||||
if branch.head == target.sha {
|
||||
// there are no commits on the branch, so we can just update the head to the new target and calculate the new tree
|
||||
let branch = branch::Branch {
|
||||
head: new_target_commit.id(),
|
||||
tree: branch_merge_index.write_tree_to(repo)?,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
let branch_head_commit = repo.find_commit(branch.head).context(format!(
|
||||
"failed to find commit {} for branch {}",
|
||||
branch.head, branch.id
|
||||
))?;
|
||||
let branch_head_tree = branch_head_commit.tree().context(format!(
|
||||
"failed to find tree for commit {} for branch {}",
|
||||
branch.head, branch.id
|
||||
// try to merge branch head with new target
|
||||
let branch_head_commit = repo.find_commit(branch.head).context(format!(
|
||||
"failed to find commit {} for branch {}",
|
||||
branch.head, branch.id
|
||||
))?;
|
||||
let branch_head_tree = branch_head_commit.tree().context(format!(
|
||||
"failed to find tree for commit {} for branch {}",
|
||||
branch.head, branch.id
|
||||
))?;
|
||||
let mut branch_head_merge_index = repo
|
||||
.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree)
|
||||
.context(format!(
|
||||
"failed to merge head tree for branch {}",
|
||||
branch.id
|
||||
))?;
|
||||
|
||||
let branch_has_commits = branch.head != target.sha;
|
||||
let branch_head_merge_index = branch_has_commits
|
||||
.then(|| {
|
||||
repo.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree)
|
||||
.context(format!(
|
||||
"failed to merge head tree for branch {}",
|
||||
branch.id
|
||||
))
|
||||
})
|
||||
.transpose()?;
|
||||
if branch_head_merge_index.has_conflicts() {
|
||||
// branch commits conflict with new target, make sure the branch is
|
||||
// unapplied. conflicts witll be dealt with when applying it back.
|
||||
let branch = branch::Branch {
|
||||
applied: false,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
if branch_merge_index.has_conflicts() {
|
||||
if branch.applied {
|
||||
// unapply branch for now. we'll handle it later, when user applies it back.
|
||||
if let Some(unapplied_branch) = super::unapply_branch(gb_repository, project_repository, &branch.id)
|
||||
.context("failed to unapply branch")? {
|
||||
branch = unapplied_branch;
|
||||
} else {
|
||||
// branch was removed, so we are done
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
// branch commits do not conflict with new target, so lets merge them
|
||||
let branch_head_merge_tree_oid =
|
||||
branch_head_merge_index
|
||||
.write_tree_to(repo)
|
||||
.context(format!(
|
||||
"failed to write head merge index for {}",
|
||||
branch.id
|
||||
))?;
|
||||
|
||||
if let Some(mut branch_head_merge_index) = branch_head_merge_index {
|
||||
// there are commits on this branch, try to merge them with a new tree
|
||||
if !branch_head_merge_index.has_conflicts() {
|
||||
// does not conflict with head, so lets merge it and update the head
|
||||
let merge_tree_oid = branch_head_merge_index
|
||||
.write_tree_to(repo)
|
||||
.context("failed to write tree")?;
|
||||
// get tree from merge_tree_oid
|
||||
let merge_tree = repo
|
||||
.find_tree(merge_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
// commit the merge tree oid
|
||||
let new_branch_head = project_repository
|
||||
.commit(
|
||||
user,
|
||||
"merged upstream (head only)",
|
||||
&merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
let branch = branch::Branch {
|
||||
head: new_branch_head,
|
||||
tree: merge_tree_oid,
|
||||
..branch.clone()
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
Ok(Some(branch))
|
||||
} else {
|
||||
// branch commits conflict with new target, branch is unapplied. we are done.
|
||||
Ok(Some(branch))
|
||||
}
|
||||
} else {
|
||||
// no commits, branch is unapplied and conflicts with new target. we are done.
|
||||
Ok(Some(branch))
|
||||
}
|
||||
} else {
|
||||
// branch tree does not have conflicts with new target.
|
||||
if let Some(mut branch_head_merge_index) = branch_head_merge_index {
|
||||
// there are commits on this branch, try to merge them with a new tree
|
||||
if branch_head_merge_index.has_conflicts() {
|
||||
// if branch commits conflict with new target, unapply branch.
|
||||
if branch.applied {
|
||||
// branch comits conflict with new targtet, unapply branch
|
||||
super::unapply_branch(gb_repository, project_repository, &branch.id)
|
||||
.context("failed to unapply branch")?;
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
let branch_head_merge_tree_oid = branch_head_merge_index
|
||||
.write_tree_to(repo)
|
||||
.context(format!("failed to write head merge index for {}", branch.id))?;
|
||||
|
||||
let non_commited_files = diff::trees(
|
||||
&project_repository.git_repository,
|
||||
&branch_head_tree,
|
||||
&branch_tree,
|
||||
)?;
|
||||
|
||||
// if the merge_tree is the same as the new_target_tree and there are no uncommitted changes
|
||||
// then the vbranch is fully merged, so delete it
|
||||
if branch_head_merge_tree_oid == new_target_tree.id()
|
||||
&& non_commited_files.is_empty()
|
||||
{
|
||||
branch_writer.delete(&branch)?;
|
||||
Ok(None)
|
||||
} else {
|
||||
let new_branch_head = if branch.upstream.is_some() {
|
||||
// if the branch was pushed, create a merge commit to avoid force pushing.
|
||||
|
||||
let branch_head_merge_tree = repo
|
||||
.find_tree(branch_head_merge_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
project_repository
|
||||
.commit(
|
||||
user,
|
||||
"merged upstream",
|
||||
&branch_head_merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to commit merge")?
|
||||
} else {
|
||||
// branch was not pushed yet. attempt to rebase, if it fails, do a merge commit.
|
||||
|
||||
let (_, committer) = project_repository.git_signatures(user)?;
|
||||
let annotated_branch_head = repo
|
||||
.find_annotated_commit(branch.head)
|
||||
.context("failed to find annotated commit")?;
|
||||
let annotated_upstream_base = repo
|
||||
.find_annotated_commit(new_target_commit.id())
|
||||
.context("failed to find annotated 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 {
|
||||
// Finish the rebase.
|
||||
rebase.finish(None).context("failed to finish rebase")?;
|
||||
last_rebase_head
|
||||
} else {
|
||||
// abort the rebase, just do a merge
|
||||
rebase.abort().context("failed to abort rebase")?;
|
||||
|
||||
// get tree from merge_tree_oid
|
||||
let merge_tree = repo
|
||||
.find_tree(branch_head_merge_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
// commit the merge tree oid
|
||||
project_repository
|
||||
.commit(
|
||||
user,
|
||||
"merged upstream",
|
||||
&merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to commit merge")?
|
||||
}
|
||||
};
|
||||
|
||||
let branch = branch::Branch {
|
||||
head: new_branch_head,
|
||||
tree: branch_merge_index
|
||||
.write_tree_to(repo)
|
||||
.context("failed to write tree")?,
|
||||
..branch.clone()
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
Ok(Some(branch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let branch = branch::Branch {
|
||||
head: new_target_commit.id(),
|
||||
tree: branch_merge_index
|
||||
.write_tree_to(repo)
|
||||
.context("failed to write tree")?,
|
||||
..branch.clone()
|
||||
};
|
||||
// there were no conflicts and no commits, so write the merge index as the new tree and update the head to the new target
|
||||
branch_writer.write(&branch)?;
|
||||
Ok(Some(branch))
|
||||
}
|
||||
if branch_head_merge_tree_oid == new_target_tree.id() {
|
||||
// after merging the branch head with the new target the tree is the
|
||||
// same as the new target tree. meaning we can safely use the new target commit
|
||||
// as the new branch head.
|
||||
let non_commited_files = diff::trees(
|
||||
&project_repository.git_repository,
|
||||
&branch_head_tree,
|
||||
&branch_tree,
|
||||
)?;
|
||||
if non_commited_files.is_empty() {
|
||||
// if there are no commited files, then the branch is fully merged
|
||||
// and we can delete it.
|
||||
branch_writer.delete(&branch)?;
|
||||
project_repository.delete_branch_reference(&branch)?;
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
)
|
||||
// there are some uncommied files left. we should put them into the branch
|
||||
// tree.
|
||||
let branch = branch::Branch {
|
||||
head: new_target_commit.id(),
|
||||
tree: branch_merge_index.write_tree_to(repo)?,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
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 branch_head_merge_tree = repo
|
||||
.find_tree(branch_head_merge_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
let new_target_head = project_repository
|
||||
.commit(
|
||||
user,
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
target.branch.remote(),
|
||||
target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&branch_head_merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
let branch = branch::Branch {
|
||||
head: new_target_head,
|
||||
tree: branch_merge_index.write_tree_to(repo)?,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
// 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 commit")?;
|
||||
let annotated_upstream_base = repo
|
||||
.find_annotated_commit(new_target_commit.id())
|
||||
.context("failed to find annotated 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")?;
|
||||
let branch = branch::Branch {
|
||||
head: last_rebase_head,
|
||||
tree: branch_merge_index.write_tree_to(repo)?,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
// 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(branch_head_merge_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
// commit the merge tree oid
|
||||
let new_branch_head = project_repository
|
||||
.commit(
|
||||
user,
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
target.branch.remote(),
|
||||
target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
let branch = branch::Branch {
|
||||
head: new_branch_head,
|
||||
tree: branch_merge_index.write_tree_to(repo)?,
|
||||
..branch
|
||||
};
|
||||
branch_writer.write(&branch)?;
|
||||
Ok(Some(branch))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
@ -2832,8 +2832,6 @@ fn test_apply_out_of_date_vbranch() -> Result<()> {
|
||||
assert_eq!(branches.len(), 1); // one is there still
|
||||
let branch1 = &branches.iter().find(|b| &b.id == branch_id).unwrap();
|
||||
assert_eq!(branch1.files.len(), 0);
|
||||
assert_eq!(branch1.commits.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1785,7 +1785,7 @@ pub fn write_tree(
|
||||
write_tree_onto_commit(project_repository, target.sha, files)
|
||||
}
|
||||
|
||||
fn write_tree_onto_commit(
|
||||
pub fn write_tree_onto_commit(
|
||||
project_repository: &project_repository::Repository,
|
||||
commit_oid: git::Oid,
|
||||
files: &HashMap<path::PathBuf, Vec<diff::Hunk>>,
|
||||
|
@ -1589,9 +1589,10 @@ mod update_base_branch {
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch_id);
|
||||
assert!(!branches[0].active);
|
||||
dbg!(&branches[0]);
|
||||
assert!(branches[0].base_current);
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
assert_eq!(branches[0].commits.len(), 0);
|
||||
assert!(controller
|
||||
.can_apply_virtual_branch(&project_id, &branch_id)
|
||||
.await
|
||||
@ -1614,6 +1615,10 @@ mod update_base_branch {
|
||||
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
|
||||
"second"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(repository.path().join("file2.txt")).unwrap(),
|
||||
"other"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2230,7 +2235,7 @@ mod update_base_branch {
|
||||
assert!(branches[0].active);
|
||||
assert!(branches[0].base_current);
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[0].commits.len(), 1); // TODO: should be 0
|
||||
assert_eq!(branches[0].commits.len(), 0);
|
||||
assert!(controller
|
||||
.can_apply_virtual_branch(&project_id, &branch_id)
|
||||
.await
|
||||
|
Loading…
Reference in New Issue
Block a user