allow to stash conflicting branches

This commit is contained in:
Nikita Galaiko 2023-12-18 12:59:51 +01:00 committed by GitButler
parent ba12a77379
commit 56dae673cc
3 changed files with 216 additions and 92 deletions

View File

@ -71,15 +71,15 @@ pub fn resolve(repository: &Repository, path: &str) -> Result<()> {
Ok(())
}
pub fn conflicting_files(repository: &Repository) -> Result<Option<Vec<String>>> {
pub fn conflicting_files(repository: &Repository) -> Result<Vec<String>> {
let conflicts_path = repository.git_repository.path().join("conflicts");
if !conflicts_path.exists() {
return Ok(None);
return Ok(vec![]);
}
let file = std::fs::File::open(conflicts_path)?;
let reader = std::io::BufReader::new(file);
Ok(Some(reader.lines().map_while(Result::ok).collect()))
Ok(reader.lines().map_while(Result::ok).collect())
}
pub fn is_conflicting(repository: &Repository, path: Option<&str>) -> Result<bool> {
@ -117,5 +117,10 @@ pub fn is_resolving(repository: &Repository) -> bool {
pub fn clear(repository: &Repository) -> Result<()> {
let merge_path = repository.git_repository.path().join("base_merge_parent");
std::fs::remove_file(merge_path)?;
for file in conflicting_files(repository)? {
resolve(repository, &file)?;
}
Ok(())
}

View File

@ -273,6 +273,7 @@ pub fn apply_branch(
&merge_conflicts,
Some(default_target.sha),
)?;
return Ok(());
}
@ -538,14 +539,6 @@ pub fn unapply_branch(
project_repository: &project_repository::Repository,
branch_id: &BranchId,
) -> Result<Option<branch::Branch>, errors::UnapplyBranchError> {
if conflicts::is_resolving(project_repository) {
return Err(errors::UnapplyBranchError::Conflict(
errors::ProjectConflictError {
project_id: project_repository.project().id,
},
));
}
let session = &gb_repository
.get_or_create_current_session()
.context("failed to get or create currnt session")?;
@ -577,70 +570,88 @@ pub fn unapply_branch(
})
})?;
let applied_branches = Iterator::new(&current_session_reader)
.context("failed to create branch iterator")?
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let applied_statuses = get_applied_status(
gb_repository,
project_repository,
&default_target,
applied_branches,
)
.context("failed to get status by branch")?;
let status = applied_statuses
.iter()
.find(|(s, _)| s.id == target_branch.id)
.context("failed to find status for branch");
let branch_writer = branch::Writer::new(gb_repository);
if let Ok((_, files)) = status {
if files.is_empty() {
// if there is nothing to unapply, remove the branch straight away
branch_writer
.delete(&target_branch)
.context("Failed to remove branch")?;
project_repository.delete_branch_reference(&target_branch)?;
return Ok(None);
}
target_branch.tree = write_tree(project_repository, &default_target, files)?;
target_branch.applied = false;
branch_writer.write(&mut target_branch)?;
}
let repo = &project_repository.git_repository;
let target_commit = repo
.find_commit(default_target.sha)
.context("failed to find target commit")?;
// ok, update the wd with the union of the rest of the branches
let base_tree = target_commit.tree().context("failed to get target tree")?;
let final_tree = if conflicts::is_resolving(project_repository) {
// when applying branch leads to a conflict, all other branches are unapplied.
// this means we can just reset to the default target tree.
{
let branch_writer = branch::Writer::new(gb_repository);
target_branch.applied = false;
branch_writer.write(&mut target_branch)?;
}
// go through the other applied branches and merge them into the final tree
// then check that out into the working directory
let final_tree = applied_statuses
.into_iter()
.filter(|(branch, _)| &branch.id != branch_id)
.fold(
target_commit.tree().context("failed to get target tree"),
|final_tree, status| {
let final_tree = final_tree?;
let tree_oid = write_tree(project_repository, &default_target, &status.1)?;
let branch_tree = repo.find_tree(tree_oid)?;
let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree)?;
let final_tree_oid = result.write_tree_to(repo)?;
repo.find_tree(final_tree_oid)
.context("failed to find tree")
},
)?;
conflicts::clear(project_repository).context("failed to clear conflicts")?;
target_commit.tree().context("failed to get target tree")?
} else {
// if we are not resolving, we need to merge the rest of the applied branches
let applied_branches = Iterator::new(&current_session_reader)
.context("failed to create branch iterator")?
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let applied_statuses = get_applied_status(
gb_repository,
project_repository,
&default_target,
applied_branches,
)
.context("failed to get status by branch")?;
let status = applied_statuses
.iter()
.find(|(s, _)| s.id == target_branch.id)
.context("failed to find status for branch");
let branch_writer = branch::Writer::new(gb_repository);
if let Ok((_, files)) = status {
if files.is_empty() {
// if there is nothing to unapply, remove the branch straight away
branch_writer
.delete(&target_branch)
.context("Failed to remove branch")?;
project_repository.delete_branch_reference(&target_branch)?;
return Ok(None);
}
target_branch.tree = write_tree(project_repository, &default_target, files)?;
target_branch.applied = false;
branch_writer.write(&mut target_branch)?;
}
let target_commit = repo
.find_commit(default_target.sha)
.context("failed to find target commit")?;
// ok, update the wd with the union of the rest of the branches
let base_tree = target_commit.tree().context("failed to get target tree")?;
// go through the other applied branches and merge them into the final tree
// then check that out into the working directory
applied_statuses
.into_iter()
.filter(|(branch, _)| &branch.id != branch_id)
.fold(
target_commit.tree().context("failed to get target tree"),
|final_tree, status| {
let final_tree = final_tree?;
let tree_oid = write_tree(project_repository, &default_target, &status.1)?;
let branch_tree = repo.find_tree(tree_oid)?;
let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree)?;
let final_tree_oid = result.write_tree_to(repo)?;
repo.find_tree(final_tree_oid)
.context("failed to find tree")
},
)?
};
// checkout final_tree into the working directory
repo.checkout_tree(&final_tree)
@ -913,22 +924,19 @@ pub fn calculate_non_commited_diffs(
let conflicting_files = conflicts::conflicting_files(project_repository)?;
for (file_path, non_commited_hunks) in &non_commited_diff {
let mut conflicted = false;
if let Some(conflicts) = &conflicting_files {
if conflicts.contains(&file_path.display().to_string()) {
// check file for conflict markers, resolve the file if there are none in any hunk
for hunk in non_commited_hunks {
if hunk.diff.contains("<<<<<<< ours") {
conflicted = true;
}
if hunk.diff.contains(">>>>>>> theirs") {
conflicted = true;
}
if conflicting_files.contains(&file_path.display().to_string()) {
// check file for conflict markers, resolve the file if there are none in any hunk
for hunk in non_commited_hunks {
if hunk.diff.contains("<<<<<<< ours") {
conflicted = true;
}
if !conflicted {
conflicts::resolve(project_repository, &file_path.display().to_string())
.unwrap();
if hunk.diff.contains(">>>>>>> theirs") {
conflicted = true;
}
}
if !conflicted {
conflicts::resolve(project_repository, &file_path.display().to_string()).unwrap();
}
}
}
@ -1739,13 +1747,15 @@ fn get_applied_status(
})
.collect::<Vec<_>>();
// write updated state
let branch_writer = branch::Writer::new(gb_repository);
for (vbranch, files) in &mut hunks_by_branch {
vbranch.tree = write_tree(project_repository, default_target, files)?;
branch_writer
.write(vbranch)
.context(format!("failed to write virtual branch {}", vbranch.name))?;
// write updated state if not resolving
if !project_repository.is_resolving() {
let branch_writer = branch::Writer::new(gb_repository);
for (vbranch, files) in &mut hunks_by_branch {
vbranch.tree = write_tree(project_repository, default_target, files)?;
branch_writer
.write(vbranch)
.context(format!("failed to write virtual branch {}", vbranch.name))?;
}
}
Ok(hunks_by_branch)

View File

@ -636,6 +636,118 @@ mod unapply {
assert!(!branches[0].active);
}
#[tokio::test]
async fn conflicting() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a conflicting branch, and stash it
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].base_current);
assert!(branches[0].active);
assert_eq!(branches[0].files[0].hunks[0].diff, "@@ -1 +1 @@\n-first\n\\ No newline at end of file\n+conflict\n\\ No newline at end of file\n");
controller
.unapply_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
branches[0].id
};
{
// update base branch, causing conflict
controller.update_base_branch(&project_id).await.unwrap();
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == branch_id)
.unwrap();
assert!(!branch.base_current);
assert!(!branch.active);
}
{
// apply branch, it should conflict
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(branch.base_current);
assert!(branch.conflicted);
assert_eq!(branch.files[0].hunks[0].diff, "@@ -1 +1,5 @@\n-first\n\\ No newline at end of file\n+<<<<<<< ours\n+conflict\n+=======\n+second\n+>>>>>>> theirs\n");
}
{
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(!branch.active);
assert!(!branch.base_current);
assert!(!branch.conflicted);
assert_eq!(branch.files[0].hunks[0].diff, "@@ -1 +1 @@\n-first\n\\ No newline at end of file\n+conflict\n\\ No newline at end of file\n");
}
}
#[tokio::test]
async fn delete_if_empty() {
let Test {
@ -1653,7 +1765,6 @@ 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(), 0);
@ -4104,7 +4215,6 @@ mod init {
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
dbg!(&branches);
assert_eq!(branches.len(), 1);
assert!(branches[0].files.is_empty());
assert_eq!(branches[0].commits.len(), 1);
@ -4132,7 +4242,6 @@ mod init {
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
dbg!(&branches);
assert_eq!(branches.len(), 1);
assert!(branches[0].files.is_empty());
assert_eq!(branches[0].commits.len(), 1);