simplify update branch logic

This commit is contained in:
Nikita Galaiko 2023-11-29 13:11:47 +01:00 committed by GitButler
parent 20782cec64
commit c513de478c
4 changed files with 212 additions and 235 deletions

View File

@ -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()

View File

@ -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(())
}

View File

@ -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>>,

View File

@ -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