mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-28 20:15:20 +03:00
allow to stash conflicting branches
This commit is contained in:
parent
ba12a77379
commit
56dae673cc
@ -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(())
|
||||
}
|
||||
|
@ -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(¤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(|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(¤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(|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)
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user