Merge pull request #1180 from gitbutlerapp/fix-hunk-movement

Fix hunk movement
This commit is contained in:
Nikita Galaiko 2023-09-07 17:22:58 +02:00 committed by GitHub
commit 05557f69c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 274 additions and 149 deletions

View File

@ -60,6 +60,7 @@ impl super::RunCommand for Commit {
&app.project_repository(), &app.project_repository(),
&commit_branch, &commit_branch,
&message, &message,
None,
) )
.context("failed to commit")?; .context("failed to commit")?;

View File

@ -4,7 +4,7 @@ use tracing::instrument;
use crate::{error::Error, git}; use crate::{error::Error, git};
use super::controller::Controller; use super::{branch::Ownership, controller::Controller};
#[tauri::command(async)] #[tauri::command(async)]
#[instrument(skip(handle))] #[instrument(skip(handle))]
@ -13,10 +13,11 @@ pub async fn commit_virtual_branch(
project_id: &str, project_id: &str,
branch: &str, branch: &str,
message: &str, message: &str,
ownership: Option<Ownership>,
) -> Result<(), Error> { ) -> Result<(), Error> {
handle handle
.state::<Controller>() .state::<Controller>()
.create_commit(project_id, branch, message) .create_commit(project_id, branch, message, ownership.as_ref())
.await .await
.map_err(Into::into) .map_err(Into::into)
} }

View File

@ -11,6 +11,8 @@ use crate::{
projects, users, watcher, projects, users, watcher,
}; };
use super::branch::Ownership;
pub struct Controller { pub struct Controller {
local_data_dir: path::PathBuf, local_data_dir: path::PathBuf,
semaphores: Arc<tokio::sync::Mutex<HashMap<String, Semaphore>>>, semaphores: Arc<tokio::sync::Mutex<HashMap<String, Semaphore>>>,
@ -66,11 +68,18 @@ impl Controller {
project_id: &str, project_id: &str,
branch: &str, branch: &str,
message: &str, message: &str,
ownership: Option<&Ownership>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.with_lock(project_id, || { self.with_lock(project_id, || {
self.with_verify_branch(project_id, |gb_repository, project_repository| { self.with_verify_branch(project_id, |gb_repository, project_repository| {
super::commit(gb_repository, project_repository, branch, message) super::commit(
.map_err(Error::Other) gb_repository,
project_repository,
branch,
message,
ownership,
)
.map_err(Error::Other)
}) })
}) })
.await .await

View File

@ -115,7 +115,13 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
assert_eq!(branch.commits.len(), 0); assert_eq!(branch.commits.len(), 0);
// commit // commit
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"test commit",
None,
)?;
// status (no files) // status (no files)
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
@ -205,7 +211,13 @@ fn test_track_binary_files() -> Result<()> {
); );
// commit // commit
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"test commit",
None,
)?;
// status (no files) // status (no files)
let branches = list_virtual_branches(&gb_repo, &project_repository).unwrap(); let branches = list_virtual_branches(&gb_repo, &project_repository).unwrap();
@ -226,7 +238,13 @@ fn test_track_binary_files() -> Result<()> {
file.write_all(&image_data)?; file.write_all(&image_data)?;
// commit // commit
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"test commit",
None,
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository).unwrap(); let branches = list_virtual_branches(&gb_repo, &project_repository).unwrap();
let commit_id = &branches[0].commits[0].id; let commit_id = &branches[0].commits[0].id;
@ -1040,7 +1058,13 @@ fn test_update_base_branch_base() -> Result<()> {
.expect("failed to create virtual branch") .expect("failed to create virtual branch")
.id; .id;
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"test commit",
None,
)?;
std::fs::write( std::fs::write(
std::path::Path::new(&project.path).join(file_path2), std::path::Path::new(&project.path).join(file_path2),
@ -1143,7 +1167,13 @@ fn test_update_base_branch_detect_integrated_branches() -> Result<()> {
"line1\nline2\nline3\nline4\nupstream\n", "line1\nline2\nline3\nline4\nupstream\n",
)?; )?;
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"test commit",
None,
)?;
// add something to the branch // add something to the branch
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
@ -1213,7 +1243,13 @@ fn test_update_base_branch_detect_integrated_branches_with_more_work() -> Result
"update target", "update target",
)?; )?;
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"test commit",
None,
)?;
// add some uncommitted work // add some uncommitted work
std::fs::write( std::fs::write(
@ -1462,6 +1498,7 @@ fn test_update_target_with_conflicts_in_vbranches() -> Result<()> {
&project_repository, &project_repository,
&branch7_id, &branch7_id,
"integrated commit", "integrated commit",
None,
)?; )?;
unapply_branch(&gb_repo, &project_repository, &branch7_id)?; unapply_branch(&gb_repo, &project_repository, &branch7_id)?;
@ -1510,6 +1547,7 @@ fn test_update_target_with_conflicts_in_vbranches() -> Result<()> {
&project_repository, &project_repository,
&branch2_id, &branch2_id,
"commit conflicts", "commit conflicts",
None,
)?; )?;
std::fs::write( std::fs::write(
std::path::Path::new(&project.path).join(file_path), std::path::Path::new(&project.path).join(file_path),
@ -1567,6 +1605,7 @@ fn test_update_target_with_conflicts_in_vbranches() -> Result<()> {
&project_repository, &project_repository,
&branch5_id, &branch5_id,
"broken, but will fix", "broken, but will fix",
None,
)?; )?;
std::fs::write( std::fs::write(
std::path::Path::new(&project.path).join(file_path3), std::path::Path::new(&project.path).join(file_path3),
@ -2038,6 +2077,7 @@ fn test_detect_remote_commits() -> Result<()> {
&project_repository, &project_repository,
&branch1_id, &branch1_id,
"upstream commit 1", "upstream commit 1",
None,
)?; )?;
// create another commit to push upstream // create another commit to push upstream
@ -2051,6 +2091,7 @@ fn test_detect_remote_commits() -> Result<()> {
&project_repository, &project_repository,
&branch1_id, &branch1_id,
"upstream commit 2", "upstream commit 2",
None,
)?; )?;
// push the commit upstream // push the commit upstream
@ -2070,7 +2111,13 @@ fn test_detect_remote_commits() -> Result<()> {
"line1\nline2\nline3\nline4\nupstream\nmore upstream\nmore work\n", "line1\nline2\nline3\nline4\nupstream\nmore upstream\nmore work\n",
)?; )?;
commit(&gb_repo, &project_repository, &branch1_id, "local commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"local commit",
None,
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
assert_eq!(branches.len(), 1); assert_eq!(branches.len(), 1);
@ -2425,12 +2472,14 @@ fn test_upstream_integrated_vbranch() -> Result<()> {
&project_repository, &project_repository,
&branch1_id, &branch1_id,
"integrated commit", "integrated commit",
None,
)?; )?;
commit( commit(
&gb_repo, &gb_repo,
&project_repository, &project_repository,
&branch2_id, &branch2_id,
"non-integrated commit", "non-integrated commit",
None,
)?; )?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
@ -2475,9 +2524,6 @@ fn test_partial_commit() -> Result<()> {
let branch1_id = create_virtual_branch(&gb_repo, &BranchCreateRequest::default()) let branch1_id = create_virtual_branch(&gb_repo, &BranchCreateRequest::default())
.expect("failed to create virtual branch") .expect("failed to create virtual branch")
.id; .id;
let branch2_id = create_virtual_branch(&gb_repo, &BranchCreateRequest::default())
.expect("failed to create virtual branch")
.id;
// create a change with two hunks // create a change with two hunks
std::fs::write( std::fs::write(
@ -2485,90 +2531,104 @@ fn test_partial_commit() -> Result<()> {
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\npatch2\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nmiddle\nline11\nline12\npatch3\n", "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\npatch2\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nmiddle\nline11\nline12\npatch3\n",
)?; )?;
// move hunk1 and hunk3 to branch2
let current_session = gb_repo.get_or_create_current_session()?;
let current_session_reader = sessions::Reader::open(&gb_repo, &current_session)?;
let branch_reader = branch::Reader::new(&current_session_reader);
let branch_writer = branch::Writer::new(&gb_repo);
let branch2 = branch_reader.read(&branch2_id)?;
branch_writer.write(&branch::Branch {
ownership: Ownership {
files: vec!["test.txt:9-16".parse()?],
},
..branch2
})?;
let branch1 = branch_reader.read(&branch1_id)?;
branch_writer.write(&branch::Branch {
ownership: Ownership {
files: vec!["test.txt:1-6".parse()?, "test.txt:17-24".parse()?],
},
..branch1
})?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch1.files[0].hunks.len(), 2);
let branch2 = &branches.iter().find(|b| b.id == branch2_id).unwrap(); assert_eq!(branch.files.len(), 1);
assert_eq!(branch2.files[0].hunks.len(), 1); assert_eq!(branch.files[0].hunks.len(), 3);
assert_eq!(branch.commits.len(), 0);
// commit // commit
commit(&gb_repo, &project_repository, &branch1_id, "branch1 commit")?; commit(
commit(&gb_repo, &project_repository, &branch2_id, "branch2 commit")?; &gb_repo,
&project_repository,
&branch1_id,
"partial commit",
Some(&"test.txt:17-24".parse::<Ownership>().unwrap()),
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].hunks.len(), 2);
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(branch.commits[0].files[0].hunks.len(), 1);
Ok(())
}
#[test]
fn test_commit_partial() -> Result<()> {
let TestDeps {
repository,
project,
gb_repo,
..
} = new_test_deps()?;
let project_repository = project_repository::Repository::open(&project)?;
let file_path = std::path::Path::new("test.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"file1\n",
)?;
let file_path2 = std::path::Path::new("test2.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path2),
"file2\n",
)?;
test_utils::commit_all(&repository);
let commit1_oid = repository.head().unwrap().target().unwrap();
let commit1 = repository.find_commit(commit1_oid).unwrap();
set_test_target(&gb_repo, &project_repository, &repository)?;
// remove file
std::fs::remove_file(std::path::Path::new(&project.path).join(file_path2))?;
// add new file
let file_path3 = std::path::Path::new("test3.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path3),
"file3\n",
)?;
let branch1_id = create_virtual_branch(&gb_repo, &BranchCreateRequest::default())
.expect("failed to create virtual branch")
.id;
// commit
commit(
&gb_repo,
&project_repository,
&branch1_id,
"branch1 commit",
None,
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
let branch2 = &branches.iter().find(|b| b.id == branch2_id).unwrap();
// branch one test.txt has just the 1st and 3rd hunks applied // branch one test.txt has just the 1st and 3rd hunks applied
assert_eq!( let commit2 = &branch1.commits[0].id;
branch1.commits[0].files[0] let commit2 = commit2
.path .parse::<git::Oid>()
.display() .expect("failed to parse commit id");
.to_string() let commit2 = repository
.as_str(), .find_commit(commit2)
"test.txt" .expect("failed to get commit object");
);
assert_eq!(
branch1.commits[0].files[0].hunks[0].diff,
"@@ -1,4 +1,5 @@\n line1\n+patch1\n line2\n line3\n line4\n"
);
assert_eq!(
branch1.commits[0].files[0].hunks[1].diff,
"@@ -15,5 +16,7 @@ line10\n middle\n middle\n middle\n+middle\n line11\n line12\n+patch3\n"
);
// branch two test.txt has just the middle hunk applied let tree = commit1.tree().expect("failed to get tree");
assert_eq!( let file_list = tree_to_file_list(&repository, &tree);
branch2.commits[0].files[0] assert_eq!(file_list, vec!["test.txt", "test2.txt"]);
.path
.display()
.to_string()
.as_str(),
"test.txt"
);
assert_eq!(
branch2.commits[0].files[0].hunks[0].diff,
"@@ -8,6 +8,7 @@ middle\n middle\n middle\n line6\n+patch2\n line7\n line8\n line9\n"
);
// ok, now we're going to unapply branch1, which should remove the 1st and 3rd hunks // get the tree
unapply_branch(&gb_repo, &project_repository, &branch1_id)?; let tree = commit2.tree().expect("failed to get tree");
// read contents of test.txt let file_list = tree_to_file_list(&repository, &tree);
let contents = std::fs::read_to_string(std::path::Path::new(&project.path).join(file_path))?; assert_eq!(file_list, vec!["test.txt", "test3.txt"]);
assert_eq!(contents, "line1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\npatch2\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n");
// ok, now we're going to re-apply branch1, which adds hunk 1 and 3, then unapply branch2, which should remove the middle hunk
apply_branch(&gb_repo, &project_repository, &branch1_id)?;
unapply_branch(&gb_repo, &project_repository, &branch2_id)?;
let contents = std::fs::read_to_string(std::path::Path::new(&project.path).join(file_path))?;
assert_eq!(contents, "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nmiddle\nline11\nline12\npatch3\n");
// finally, reapply the middle hunk on branch2, so we have all of them again
apply_branch(&gb_repo, &project_repository, &branch2_id)?;
let contents = std::fs::read_to_string(std::path::Path::new(&project.path).join(file_path))?;
assert_eq!(contents, "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\npatch2\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nmiddle\nline11\nline12\npatch3\n");
Ok(()) Ok(())
} }
@ -2614,7 +2674,13 @@ fn test_commit_add_and_delete_files() -> Result<()> {
.id; .id;
// commit // commit
commit(&gb_repo, &project_repository, &branch1_id, "branch1 commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"branch1 commit",
None,
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
@ -2683,7 +2749,13 @@ fn test_commit_executable_and_symlinks() -> Result<()> {
.id; .id;
// commit // commit
commit(&gb_repo, &project_repository, &branch1_id, "branch1 commit")?; commit(
&gb_repo,
&project_repository,
&branch1_id,
"branch1 commit",
None,
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
@ -3041,7 +3113,13 @@ fn test_apply_out_of_date_conflicting_vbranch() -> Result<()> {
assert!(branch1.conflicted); assert!(branch1.conflicted);
// try to commit, fail // try to commit, fail
let result = commit(&gb_repo, &project_repository, branch_id, "resolve commit"); let result = commit(
&gb_repo,
&project_repository,
branch_id,
"resolve commit",
None,
);
assert!(result.is_err()); assert!(result.is_err());
// fix the conflict and commit it // fix the conflict and commit it
@ -3060,7 +3138,13 @@ fn test_apply_out_of_date_conflicting_vbranch() -> Result<()> {
assert!(branch1.active); assert!(branch1.active);
// commit // commit
commit(&gb_repo, &project_repository, branch_id, "resolve commit")?; commit(
&gb_repo,
&project_repository,
branch_id,
"resolve commit",
None,
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?; let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches.iter().find(|b| &b.id == branch_id).unwrap(); let branch1 = &branches.iter().find(|b| &b.id == branch_id).unwrap();

View File

@ -4,7 +4,7 @@ use std::{
path, time, vec, path, time, vec,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use diffy::{apply_bytes, Patch}; use diffy::{apply_bytes, Patch};
use serde::Serialize; use serde::Serialize;
@ -1818,6 +1818,7 @@ pub fn commit(
project_repository: &project_repository::Repository, project_repository: &project_repository::Repository,
branch_id: &str, branch_id: &str,
message: &str, message: &str,
ownership: Option<&branch::Ownership>,
) -> Result<()> { ) -> Result<()> {
if conflicts::is_conflicting(project_repository, None)? { if conflicts::is_conflicting(project_repository, None)? {
bail!("cannot commit, project is in a conflicted state"); bail!("cannot commit, project is in a conflicted state");
@ -1832,70 +1833,99 @@ pub fn commit(
let statuses = get_status_by_branch(gb_repository, project_repository) let statuses = get_status_by_branch(gb_repository, project_repository)
.context("failed to get status by branch")?; .context("failed to get status by branch")?;
match statuses.iter().find(|(branch, _)| branch.id == branch_id) { let (branch, files) = statuses
None => bail!("branch {} not found", branch_id), .iter()
Some((branch, files)) => { .find(|(branch, _)| branch.id == branch_id)
let tree_oid = write_tree(project_repository, &default_target, files)?; .ok_or_else(|| anyhow!("branch {} not found", branch_id))?;
let git_repository = &project_repository.git_repository; let tree_oid = if let Some(ownership) = ownership {
let parent_commit = git_repository let files = files
.find_commit(branch.head) .iter()
.context(format!("failed to find commit {:?}", branch.head))?; .filter_map(|file| {
let tree = git_repository let hunks = file
.find_tree(tree_oid) .hunks
.context(format!("failed to find tree {:?}", tree_oid))?; .iter()
.filter(|hunk| {
// now write a commit, using a merge parent if it exists ownership
let (author, committer) = gb_repository .files
.git_signatures() .iter()
.context("failed to get git signatures")?; .find(|f| f.file_path == file.path)
let extra_merge_parent = conflicts::merge_parent(project_repository) .map(|f| {
.context("failed to get merge parent")?; f.hunks
.iter()
let commit_oid = match extra_merge_parent { .any(|h| h.start == hunk.start && h.end == hunk.end)
Some(merge_parent) => { })
let merge_parent = git_repository .unwrap_or(false)
.find_commit(merge_parent) })
.context(format!("failed to find merge parent {:?}", merge_parent))?; .cloned()
let commit_oid = git_repository .collect::<Vec<_>>();
.commit( if hunks.is_empty() {
None, None
&author, } else {
&committer, Some(VirtualBranchFile {
message, hunks,
&tree, ..file.clone()
&[&parent_commit, &merge_parent], })
)
.context("failed to commit")?;
conflicts::clear(project_repository).context("failed to clear conflicts")?;
commit_oid
} }
None => git_repository.commit( })
.collect::<Vec<_>>();
write_tree(project_repository, &default_target, &files)?
} else {
write_tree(project_repository, &default_target, files)?
};
let git_repository = &project_repository.git_repository;
let parent_commit = git_repository
.find_commit(branch.head)
.context(format!("failed to find commit {:?}", branch.head))?;
let tree = git_repository
.find_tree(tree_oid)
.context(format!("failed to find tree {:?}", tree_oid))?;
// now write a commit, using a merge parent if it exists
let (author, committer) = gb_repository
.git_signatures()
.context("failed to get git signatures")?;
let extra_merge_parent =
conflicts::merge_parent(project_repository).context("failed to get merge parent")?;
let commit_oid = match extra_merge_parent {
Some(merge_parent) => {
let merge_parent = git_repository
.find_commit(merge_parent)
.context(format!("failed to find merge parent {:?}", merge_parent))?;
let commit_oid = git_repository
.commit(
None, None,
&author, &author,
&committer, &committer,
message, message,
&tree, &tree,
&[&parent_commit], &[&parent_commit, &merge_parent],
)?, )
}; .context("failed to commit")?;
conflicts::clear(project_repository).context("failed to clear conflicts")?;
// update the virtual branch head commit_oid
let writer = branch::Writer::new(gb_repository);
writer
.write(&Branch {
tree: tree_oid,
head: commit_oid,
..branch.clone()
})
.context("failed to write branch")?;
super::integration::update_gitbutler_integration(gb_repository, project_repository)
.context("failed to update gitbutler integration")?;
Ok(())
} }
} None => {
git_repository.commit(None, &author, &committer, message, &tree, &[&parent_commit])?
}
};
// update the virtual branch head
let writer = branch::Writer::new(gb_repository);
writer
.write(&Branch {
tree: tree_oid,
head: commit_oid,
..branch.clone()
})
.context("failed to write branch")?;
super::integration::update_gitbutler_integration(gb_repository, project_repository)
.context("failed to update gitbutler integration")?;
Ok(())
} }
pub fn name_to_branch(name: &str) -> String { pub fn name_to_branch(name: &str) -> String {