fix losing work on base branch update

This commit is contained in:
Nikita Galaiko 2023-11-28 13:59:17 +01:00 committed by GitButler
parent cbb54ac72a
commit cc0739f6aa
4 changed files with 284 additions and 241 deletions

View File

@ -171,6 +171,27 @@ fn set_exclude_decoration(project_repository: &project_repository::Repository) -
Ok(())
}
fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
println!("tree id: {}", tree.id());
for entry in tree {
println!(
" entry: {} {}",
entry.name().unwrap_or_default(),
entry.id()
);
// get entry contents
let object = entry.to_object(repo).context("failed to get object")?;
let blob = object.as_blob().context("failed to get blob")?;
// convert content to string
if let Ok(content) = std::str::from_utf8(blob.content()) {
println!(" blob: {}", content);
} else {
println!(" blob: BINARY");
}
}
Ok(())
}
// try to update the target branch
// this means that we need to:
// determine if what the target branch is now pointing to is mergeable with our current working directory
@ -230,240 +251,259 @@ pub fn update_base_branch(
// 6. unapplied branch, no conflicts
let branch_writer = branch::Writer::new(gb_repository);
let vbranches = super::get_status_by_branch(gb_repository, project_repository)?;
for (branch, all_files) in &vbranches {
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))?
};
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))?
};
// 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))?;
// 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))?;
let branch_conflicting = branch_merge_index.has_conflicts();
let branch_head_commit = repo.find_commit(branch.head).context(format!(
"failed to find commit {} for branch {}",
branch.head, branch.id
))?;
let branch_has_commits = branch.head != target.sha;
let branch_head_merge_index = branch_has_commits
.then(|| {
let head_tree = branch_head_commit.tree().context(format!(
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
))?;
repo.merge_trees(&old_target_tree, &head_tree, &new_target_tree)
.context(format!(
"failed to merge head tree for branch {}",
branch.id
))
})
.transpose()?;
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_conflicting {
if branch.applied {
// unapply branch for now. we'll handle it later, when user applies it back.
super::unapply_branch(gb_repository, project_repository, &branch.id)
.context("failed to unapply branch")?;
}
if let Some(mut branch_head_merge_index) = branch_head_merge_index {
// if there are commits on this branch, so create a merge commit with the new tree
// check index for conflicts
// if it has conflicts, we just ignore it
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")?;
branch_writer.write(&branch::Branch {
head: new_branch_head,
tree: merge_tree_oid,
..branch.clone()
})?;
}
}
} else {
// get the merge tree oid from writing the index out
let merge_tree_oid = branch_merge_index
.write_tree_to(repo)
.context("failed to write tree")?;
// branch head does not have conflicts, so don't unapply it, but still try to merge it's head if there are commits
// but also remove/archive it if the branch is fully integrated
if let Some(mut branch_head_merge_index) = branch_head_merge_index {
// check index for conflicts
if branch_head_merge_index.has_conflicts() && branch.applied {
// unapply branch for now. we'll handle it later, when user applied it back.
super::unapply_branch(gb_repository, project_repository, &branch.id)
.context("failed to unapply branch")?;
}
let non_commited_files = super::calculate_non_commited_diffs(
project_repository,
branch,
&target,
all_files,
)?;
let merge_tree_oid = branch_head_merge_index
.write_tree_to(repo)
.context("failed to write tree")?;
// if the merge_tree is the same as the new_target_tree and there are no files (uncommitted changes)
// then the vbranch is fully merged, so delete it
if merge_tree_oid == new_target_tree.id() && non_commited_files.is_empty() {
branch_writer.delete(branch)?;
} else {
// check to see if these commits have already been pushed
let mut last_rebase_head = branch.head;
let new_branch_head;
if branch.upstream.is_some() {
// 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
new_branch_head = project_repository
.commit(
user,
"merged upstream",
&merge_tree,
&[&branch_head_commit, &new_target_commit],
signing_key,
)
.context("failed to commit merge")?;
} else {
let (_, committer) = project_repository.git_signatures(user)?;
// attempt to rebase, otherwise, fall back to the merge
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;
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")?;
new_branch_head = last_rebase_head;
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 {
// abort the rebase, just do a merge
rebase.abort().context("failed to abort rebase")?;
// branch was removed, so we are done
return Ok(None);
}
}
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
new_branch_head = project_repository
let new_branch_head = project_repository
.commit(
user,
"merged upstream",
"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))?;
branch_writer.write(&branch::Branch {
head: new_branch_head,
tree: merge_tree_oid,
..branch.clone()
})?;
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))
}
}
} else {
// 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::Branch {
head: new_target_commit.id(),
tree: merge_tree_oid,
..branch.clone()
})?;
}
}
}
},
)
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
// ok, now all the problematic branches have been unapplied, so we can try to merge the upstream branch into our current working directory
// first, get a new wd tree
let wd_tree = project_repository
.get_wd_tree()
.context("failed to get wd tree")?;
// ok, now all the problematic branches have been unapplied
// now we calculate and checkout new tree for the working directory
// and try to merge it
let mut merge_index = repo
.merge_trees(&old_target_tree, &wd_tree, &new_target_tree)
.context("failed to merge trees")?;
let final_tree = updated_vbranches
.into_iter()
.filter(|branch| branch.applied)
.fold(new_target_commit.tree(), |final_tree, branch| {
let final_tree = final_tree?;
let branch_tree = repo.find_tree(branch.tree)?;
let mut merge_result = repo.merge_trees(&new_target_tree, &final_tree, &branch_tree)?;
let final_tree_oid = merge_result.write_tree_to(repo)?;
repo.find_tree(final_tree_oid)
})
.context("failed to calculate final tree")?;
if merge_index.has_conflicts() {
return Err(errors::UpdateBaseBranchError::Other(anyhow::anyhow!(
"this should not have happened, we should have already detected this"
)));
}
// now we can try to merge the upstream branch into our current working directory
repo.checkout_index(&mut merge_index).force().checkout().context(
repo.checkout_tree(&final_tree).force().checkout().context(
"failed to checkout index, this should not have happened, we should have already detected this",
)?;

View File

@ -743,7 +743,9 @@ impl ControllerInner {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |gb_repository, project_repository, _| {
super::unapply_branch(gb_repository, project_repository, branch_id).map_err(Into::into)
super::unapply_branch(gb_repository, project_repository, branch_id)
.map(|_| ())
.map_err(Into::into)
})
}

View File

@ -433,7 +433,7 @@ pub fn unapply_branch(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
branch_id: &BranchId,
) -> Result<(), errors::UnapplyBranchError> {
) -> Result<Option<branch::Branch>, errors::UnapplyBranchError> {
if conflicts::is_resolving(project_repository) {
return Err(errors::UnapplyBranchError::Conflict(
errors::ProjectConflictError {
@ -451,7 +451,7 @@ pub fn unapply_branch(
let branch_reader = branch::Reader::new(&current_session_reader);
let target_branch = branch_reader.read(branch_id).map_err(|error| match error {
let mut target_branch = branch_reader.read(branch_id).map_err(|error| match error {
reader::Error::NotFound => {
errors::UnapplyBranchError::BranchNotFound(errors::BranchNotFoundError {
project_id: project_repository.project().id,
@ -462,7 +462,7 @@ pub fn unapply_branch(
})?;
if !target_branch.applied {
return Ok(());
return Ok(Some(target_branch));
}
let default_target = get_default_target(&current_session_reader)
@ -503,15 +503,12 @@ pub fn unapply_branch(
.context("Failed to remove branch")?;
project_repository.delete_branch_reference(&target_branch)?;
return Ok(());
return Ok(None);
}
let tree = write_tree(project_repository, &default_target, files)?;
branch_writer.write(&branch::Branch {
tree,
applied: false,
..target_branch
})?;
target_branch.tree = write_tree(project_repository, &default_target, files)?;
target_branch.applied = false;
branch_writer.write(&target_branch)?;
}
let repo = &project_repository.git_repository;
@ -550,7 +547,7 @@ pub fn unapply_branch(
super::integration::update_gitbutler_integration(gb_repository, project_repository)?;
Ok(())
Ok(Some(target_branch))
}
fn unapply_all_branches(
@ -817,11 +814,15 @@ pub fn calculate_non_commited_diffs(
return Ok(files.clone());
};
// get the trees
let target_plus_wd_oid = write_tree(project_repository, default_target, files)?;
let target_plus_wd = project_repository
.git_repository
.find_tree(target_plus_wd_oid)?;
let branch_tree = if branch.applied {
let target_plus_wd_oid = write_tree(project_repository, default_target, files)?;
project_repository
.git_repository
.find_tree(target_plus_wd_oid)
} else {
project_repository.git_repository.find_tree(branch.tree)
}?;
let branch_head = project_repository
.git_repository
.find_commit(branch.head)?
@ -831,7 +832,7 @@ pub fn calculate_non_commited_diffs(
let non_commited_diff = diff::trees(
&project_repository.git_repository,
&branch_head,
&target_plus_wd,
&branch_tree,
)
.context("failed to diff trees")?;
@ -1395,7 +1396,7 @@ pub fn virtual_hunks_by_filepath(
.collect::<HashMap<_, _>>()
}
type BranchStatus = HashMap<path::PathBuf, Vec<diff::Hunk>>;
pub type BranchStatus = HashMap<path::PathBuf, Vec<diff::Hunk>>;
// list the virtual branches and their file statuses (statusi?)
pub fn get_status_by_branch(
@ -1472,6 +1473,7 @@ fn get_non_applied_status(
.git_repository
.find_tree(branch.tree)
.context(format!("failed to find tree {}", branch.tree))?;
let target_tree = project_repository
.git_repository
.find_commit(default_target.sha)

View File

@ -1429,7 +1429,7 @@ mod update_base_branch {
.await
.unwrap();
let _branch_id = {
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
@ -1441,6 +1441,10 @@ mod update_base_branch {
.create_commit(&project_id, &branch_id, "second", None)
.await
.unwrap();
// more local work in the same branch
fs::write(repository.path().join("file2.txt"), "other").unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
@ -1455,18 +1459,15 @@ mod update_base_branch {
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
repository.merge(&branch.upstream.as_ref().unwrap().name);
repository.fetch();
}
// more local work in the same branch
fs::write(repository.path().join("file2.txt"), "other").unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
@ -1477,18 +1478,16 @@ mod update_base_branch {
// should remove integrated commit, but leave work
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0); // TODO: should be 1
// TODO: this should be uncommented
// assert_eq!(branches[0].id, branch_id);
// assert!(!branches[0].active);
// assert!(branches[0].base_current);
// assert_eq!(branches[0].files.len(), 1);
// assert_eq!(branches[0].commits.len(), 0);
// assert!(controller
// .can_apply_virtual_branch(&project_id, &branch_id)
// .await
// .unwrap());
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
}