gitbutler/crates/gitbutler-branch-actions/tests/extra/mod.rs
2024-09-13 16:58:42 +02:00

2124 lines
70 KiB
Rust

use std::{
collections::HashMap,
io::Write,
path::{Path, PathBuf},
str::FromStr,
};
#[cfg(target_family = "unix")]
use std::{
fs::Permissions,
os::unix::{fs::symlink, prelude::*},
};
use anyhow::{Context, Result};
use git2::TreeEntry;
use gitbutler_branch::{
BranchCreateRequest, BranchOwnershipClaims, BranchUpdateRequest, Target, VirtualBranchesHandle,
};
use gitbutler_branch_actions::{
get_applied_status, internal, update_workspace_commit, verify_branch, BranchManagerExt, Get,
};
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::CommitHeadersV2};
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::RepositoryExt;
use gitbutler_testsupport::{commit_all, virtual_branches::set_test_target, Case, Suite};
use pretty_assertions::assert_eq;
#[test]
fn commit_on_branch_then_change_file_then_get_status() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "line1\nline2\nline3\nline4\n"),
(PathBuf::from("test2.txt"), "line5\nline6\nline7\nline8\n"),
]));
set_test_target(ctx)?;
let mut guard = project.exclusive_worktree_access();
let branch1_id = ctx
.branch_manager()
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\n",
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.commits.len(), 0);
// commit
internal::commit(ctx, branch1_id, "test commit", None, false)?;
// status (no files)
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits.len(), 1);
std::fs::write(
Path::new(&project.path).join("test2.txt"),
"line5\nline6\nlineBLAH\nline7\nline8\n",
)?;
// should have just the last change now, the other line is committed
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.commits.len(), 1);
Ok(())
}
#[test]
fn track_binary_files() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
let file_path2 = Path::new("test2.txt");
std::fs::write(
Path::new(&project.path).join(file_path2),
"line5\nline6\nline7\nline8\n",
)?;
// add a binary file
let image_data: [u8; 12] = [
255, 0, 0, // Red pixel
0, 0, 255, // Blue pixel
255, 255, 0, // Yellow pixel
0, 255, 0, // Green pixel
];
let mut file = std::fs::File::create(Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?;
commit_all(ctx.repository());
set_test_target(ctx)?;
let mut guard = project.exclusive_worktree_access();
let branch1_id = ctx
.branch_manager()
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
// test file change
std::fs::write(
Path::new(&project.path).join(file_path2),
"line5\nline6\nline7\nline8\nline9\n",
)?;
// add a binary file
let image_data: [u8; 12] = [
255, 0, 0, // Red pixel
0, 255, 0, // Green pixel
0, 0, 255, // Blue pixel
255, 255, 0, // Yellow pixel
];
let mut file = std::fs::File::create(Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 2);
let img_file = &branch
.files
.iter()
.find(|b| b.path.as_os_str() == "image.bin")
.unwrap();
assert!(img_file.binary);
let img_oid_hex = "944996dd82015a616247c72b251e41661e528ae1";
assert_eq!(
img_file.hunks[0].diff, img_oid_hex,
"the binary file was stored in the ODB as otherwise we wouldn't have its contents. \
It cannot easily be reconstructed from the diff-lines, or we don't attempt it."
);
// commit
internal::commit(ctx, branch1_id, "test commit", None, false)?;
// status (no files)
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission()).unwrap();
let commit_id = &branches[0].commits[0].id;
let commit_obj = ctx.repository().find_commit(commit_id.to_owned())?;
let tree = commit_obj.tree()?;
let files = tree_to_entry_list(ctx.repository(), &tree);
assert_eq!(files[0].0, "image.bin");
assert_eq!(
files[0].3, img_oid_hex,
"our vbranch commit tree references the binary object we previously stored"
);
let image_data: [u8; 12] = [
0, 255, 0, // Green pixel
255, 0, 0, // Red pixel
255, 255, 0, // Yellow pixel
0, 0, 255, // Blue pixel
];
let mut file = std::fs::File::create(Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?;
// commit
internal::commit(ctx, branch1_id, "test commit", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission()).unwrap();
let commit_id = &branches[0].commits[0].id;
// get tree from commit_id
let commit_obj = ctx.repository().find_commit(commit_id.to_owned())?;
let tree = commit_obj.tree()?;
let files = tree_to_entry_list(ctx.repository(), &tree);
assert_eq!(files[0].0, "image.bin");
assert_eq!(files[0].3, "ea6901a04d1eed6ebf6822f4360bda9f008fa317");
Ok(())
}
#[test]
fn create_branch_with_ownership() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case();
set_test_target(ctx)?;
let file_path = Path::new("test.txt");
std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n").unwrap();
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch0 = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch");
get_applied_status(ctx, None).expect("failed to get status");
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
let branch0 = vb_state.get_branch_in_workspace(branch0.id).unwrap();
let branch1 = branch_manager
.create_virtual_branch(
&BranchCreateRequest {
ownership: Some(branch0.ownership),
..Default::default()
},
guard.write_permission(),
)
.expect("failed to create virtual branch");
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 2);
assert_eq!(files_by_branch_id[&branch0.id].len(), 0);
assert_eq!(files_by_branch_id[&branch1.id].len(), 1);
Ok(())
}
#[test]
fn create_branch_in_the_middle() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
branch_manager
.create_virtual_branch(
&BranchCreateRequest::default(),
project.exclusive_worktree_access().write_permission(),
)
.expect("failed to create virtual branch");
branch_manager
.create_virtual_branch(
&BranchCreateRequest::default(),
project.exclusive_worktree_access().write_permission(),
)
.expect("failed to create virtual branch");
branch_manager
.create_virtual_branch(
&BranchCreateRequest {
order: Some(1),
..Default::default()
},
project.exclusive_worktree_access().write_permission(),
)
.expect("failed to create virtual branch");
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
let mut branches = vb_state
.list_branches_in_workspace()
.expect("failed to read branches");
branches.sort_by_key(|b| b.order);
assert_eq!(branches.len(), 3);
assert_eq!(branches[0].name, "Virtual branch");
assert_eq!(branches[1].name, "Virtual branch 2");
assert_eq!(branches[2].name, "Virtual branch 1");
Ok(())
}
#[test]
fn create_branch_no_arguments() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
branch_manager
.create_virtual_branch(
&BranchCreateRequest::default(),
project.exclusive_worktree_access().write_permission(),
)
.expect("failed to create virtual branch");
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
let branches = vb_state
.list_branches_in_workspace()
.expect("failed to read branches");
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].name, "Virtual branch");
assert_eq!(branches[0].ownership, BranchOwnershipClaims::default());
assert_eq!(branches[0].order, 0);
Ok(())
}
#[test]
fn hunk_expantion() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
set_test_target(ctx)?;
let file_path = Path::new("test.txt");
std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 2);
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
// even though selected branch has changed
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch1_id,
order: Some(1),
..Default::default()
},
)?;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch2_id,
order: Some(0),
..Default::default()
},
)?;
// a slightly different hunk should still go to the same branch
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\n",
)?;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 2);
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
Ok(())
}
#[test]
fn get_status_files_by_branch_no_hunks_no_branches() -> Result<()> {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
set_test_target(ctx)?;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
assert_eq!(statuses.len(), 0);
Ok(())
}
#[test]
fn get_status_files_by_branch() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
set_test_target(ctx)?;
let file_path = Path::new("test.txt");
std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 2);
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
Ok(())
}
#[test]
fn move_hunks_multiple_sources() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\n",
)]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch3_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\n",
)?;
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
let mut branch2 = vb_state.get_branch_in_workspace(branch2_id)?;
branch2.ownership = BranchOwnershipClaims {
claims: vec!["test.txt:1-5".parse()?],
};
vb_state.set_branch(branch2.clone())?;
let mut branch1 = vb_state.get_branch_in_workspace(branch1_id)?;
branch1.ownership = BranchOwnershipClaims {
claims: vec!["test.txt:11-15".parse()?],
};
vb_state.set_branch(branch1.clone())?;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 3);
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
// assert_eq!(files_by_branch_id[&branch1_id][0].hunks.len(), 1);
assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
// assert_eq!(files_by_branch_id[&branch2_id][0].hunks.len(), 1);
assert_eq!(files_by_branch_id[&branch3_id].len(), 0);
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch3_id,
ownership: Some("test.txt:1-5,11-15".parse()?),
..Default::default()
},
)?;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 3);
assert_eq!(files_by_branch_id[&branch1_id].len(), 0);
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
assert_eq!(files_by_branch_id[&branch3_id].len(), 1);
assert_eq!(
files_by_branch_id[&branch3_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks
.len(),
2
);
assert_eq!(
files_by_branch_id[&branch3_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks[0]
.diff,
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
);
assert_eq!(
files_by_branch_id[&branch3_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks[1]
.diff,
"@@ -10,3 +11,4 @@ line9\n line10\n line11\n line12\n+line13\n"
);
Ok(())
}
#[test]
fn move_hunks_partial_explicitly() -> Result<()> {
let suite = Suite::default();
let Case {
ctx,
project,
..
} = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\n",
)]));
set_test_target(ctx)?;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\n",
)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 2);
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
// assert_eq!(files_by_branch_id[&branch1_id][0].hunks.len(), 2);
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch2_id,
ownership: Some("test.txt:1-5".parse()?),
..Default::default()
},
)?;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
let files_by_branch_id = statuses
.iter()
.map(|(branch, files)| (branch.id, files))
.collect::<HashMap<_, _>>();
assert_eq!(files_by_branch_id.len(), 2);
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
assert_eq!(
files_by_branch_id[&branch1_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks
.len(),
1
);
assert_eq!(
files_by_branch_id[&branch1_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks[0]
.diff,
"@@ -11,3 +12,4 @@ line10\n line11\n line12\n line13\n+line14\n"
);
assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
assert_eq!(
files_by_branch_id[&branch2_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks
.len(),
1
);
assert_eq!(
files_by_branch_id[&branch2_id]
.get(Path::new("test.txt"))
.unwrap()
.hunks[0]
.diff,
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
);
Ok(())
}
#[test]
fn add_new_hunk_to_the_end() -> Result<()> {
let suite = Suite::default();
let Case {
ctx,
project,
..
} = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline13\nline14\n",
)]));
set_test_target(ctx)?;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15\n",
)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch");
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
assert_eq!(
statuses[0].1.get(Path::new("test.txt")).unwrap().hunks[0].diff,
"@@ -11,5 +11,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
);
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15\n",
)?;
let statuses = get_applied_status(ctx, None)
.expect("failed to get status")
.branches;
assert_eq!(
statuses[0].1.get(Path::new("test.txt")).unwrap().hunks[0].diff,
"@@ -11,5 +12,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
);
assert_eq!(
statuses[0].1.get(Path::new("test.txt")).unwrap().hunks[1].diff,
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
);
Ok(())
}
#[test]
fn commit_id_can_be_generated_or_specified() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
commit_all(ctx.repository());
// lets make sure a change id is generated
let target_oid = ctx.repository().head().unwrap().target().unwrap();
let target = ctx.repository().find_commit(target_oid).unwrap();
let change_id = target.change_id();
// make sure we created a change-id
assert!(change_id.is_some());
// ok, make another change and specify a change-id
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nline5\n",
)?;
let repository = ctx.repository();
let mut index = repository.index().expect("failed to get index");
index
.add_all(["."], git2::IndexAddOption::DEFAULT, None)
.expect("failed to add all");
index.write().expect("failed to write index");
let oid = index.write_tree().expect("failed to write tree");
let signature = git2::Signature::now("test", "test@email.com").unwrap();
let head = repository.head().expect("failed to get head");
let refname: Refname = head.name().unwrap().parse().unwrap();
ctx.repository()
.commit_with_signature(
Some(&refname),
&signature,
&signature,
"some commit",
&repository.find_tree(oid).expect("failed to find tree"),
&[&repository
.find_commit(
repository
.refname_to_id("HEAD")
.expect("failed to get head"),
)
.expect("failed to find commit")],
// The change ID should always be generated by calling CommitHeadersV2::new
Some(CommitHeadersV2 {
change_id: "my-change-id".to_string(),
conflicted: None,
}),
)
.expect("failed to commit");
let target_oid = ctx.repository().head().unwrap().target().unwrap();
let target = ctx.repository().find_commit(target_oid).unwrap();
let change_id = target.change_id();
// the change id should be what we specified, rather than randomly generated
assert_eq!(change_id, Some("my-change-id".to_string()));
Ok(())
}
#[test]
fn merge_vbranch_upstream_clean_rebase() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &mut suite.new_case();
// create a commit and set the target
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
commit_all(ctx.repository());
let target_oid = ctx.repository().head().unwrap().target().unwrap();
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
// add a commit to the target branch it's pointing to so there is something "upstream"
commit_all(ctx.repository());
let last_push = ctx.repository().head().unwrap().target().unwrap();
// coworker adds some work
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\ncoworker work\n",
)?;
commit_all(ctx.repository());
let coworker_work = ctx.repository().head().unwrap().target().unwrap();
//update repo ref refs/remotes/origin/master to up_target oid
ctx.repository().reference(
"refs/remotes/origin/master",
coworker_work,
true,
"update target",
)?;
// revert to our file
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
set_test_target(ctx)?;
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
vb_state.set_default_target(Target {
branch: "refs/remotes/origin/master".parse().unwrap(),
remote_url: "origin".to_string(),
sha: target_oid,
push_remote_name: None,
})?;
// add some uncommitted work
let file_path2 = Path::new("test2.txt");
std::fs::write(Path::new(&project.path).join(file_path2), "file2\n")?;
// Update workspace commit
update_workspace_commit(&vb_state, ctx)?;
let remote_branch: RemoteRefname = "refs/remotes/origin/master".parse().unwrap();
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let mut branch = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch");
branch.upstream = Some(remote_branch.clone());
branch.head = last_push;
vb_state.set_branch(branch.clone())?;
// create the branch
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
assert_eq!(branches.len(), 1);
let branch1 = &branches[0];
assert_eq!(
branch1.files.len(),
1 + 1,
"'test' (modified compared to index) and 'test2' (untracked).\
This is actually correct when looking at the git repository"
);
assert_eq!(branch1.commits.len(), 1);
// assert_eq!(branch1.upstream.as_ref().unwrap().commits.len(), 1);
internal::integrate_upstream_commits(ctx, branch1.id)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches[0];
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
assert_eq!(
"line1\nline2\nline3\nline4\nupstream\ncoworker work\n",
String::from_utf8(contents)?
);
let contents = std::fs::read(Path::new(&project.path).join(file_path2))?;
assert_eq!("file2\n", String::from_utf8(contents)?);
assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 2);
// assert_eq!(branch1.upstream.as_ref().unwrap().commits.len(), 0);
Ok(())
}
#[test]
fn merge_vbranch_upstream_conflict() -> Result<()> {
let suite = Suite::default();
let mut case = suite.new_case();
case = case.refresh(&suite);
let ctx = &case.ctx;
let project = &case.project;
// create a commit and set the target
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
commit_all(ctx.repository());
let target_oid = ctx.repository().head().unwrap().target().unwrap();
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
// add a commit to the target branch it's pointing to so there is something "upstream"
commit_all(ctx.repository());
let last_push = ctx.repository().head().unwrap().target().unwrap();
// coworker adds some work
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\ncoworker work\n",
)?;
commit_all(ctx.repository());
let coworker_work = ctx.repository().head().unwrap().target().unwrap();
//update repo ref refs/remotes/origin/master to up_target oid
ctx.repository().reference(
"refs/remotes/origin/master",
coworker_work,
true,
"update target",
)?;
// revert to our file
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
set_test_target(ctx)?;
let vb_state = VirtualBranchesHandle::new(project.gb_dir());
vb_state.set_default_target(Target {
branch: "refs/remotes/origin/master".parse().unwrap(),
remote_url: "origin".to_string(),
sha: target_oid,
push_remote_name: None,
})?;
// add some uncommitted work
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\nother side\n",
)?;
let remote_branch: RemoteRefname = "refs/remotes/origin/master".parse().unwrap();
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let mut branch = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch");
branch.upstream = Some(remote_branch.clone());
branch.head = last_push;
vb_state.set_branch(branch.clone())?;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch.id,
allow_rebasing: Some(false),
..Default::default()
},
)
.unwrap();
// create the branch
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches[0];
assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 1);
// assert_eq!(branch1.upstream.as_ref().unwrap().commits.len(), 1);
internal::integrate_upstream_commits(ctx, branch1.id)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches[0];
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
assert_eq!(
"line1\nline2\nline3\nline4\nupstream\n<<<<<<< ours\nother side\n=======\ncoworker work\n>>>>>>> theirs\n",
String::from_utf8(contents)?
);
assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 1);
assert!(branch1.conflicted);
// fix the conflict
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\nother side\ncoworker work\n",
)?;
// make gb see the conflict resolution
update_workspace_commit(&vb_state, ctx)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
assert!(branches[0].conflicted);
// commit the merge resolution
internal::commit(ctx, branch1.id, "fix merge conflict", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches[0];
assert!(!branch1.conflicted);
assert_eq!(branch1.files.len(), 0);
assert_eq!(branch1.commits.len(), 3);
// make sure the last commit was a merge commit (2 parents)
let last_id = &branch1.commits[0].id;
let last_commit = ctx.repository().find_commit(last_id.to_owned())?;
assert_eq!(last_commit.parent_count(), 2);
Ok(())
}
#[test]
fn unapply_ownership_partial() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\n",
)]));
set_test_target(ctx)?;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line1\nline2\nline3\nline4\nbranch1\n",
)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch");
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].ownership.claims.len(), 1);
assert_eq!(branches[0].files[0].hunks.len(), 1);
assert_eq!(branches[0].ownership.claims[0].hunks.len(), 1);
assert_eq!(
std::fs::read_to_string(Path::new(&project.path).join("test.txt"))?,
"line1\nline2\nline3\nline4\nbranch1\n"
);
internal::unapply_ownership(
ctx,
&"test.txt:2-6".parse().unwrap(),
guard.write_permission(),
)
.unwrap();
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].ownership.claims.len(), 0);
assert_eq!(
std::fs::read_to_string(Path::new(&project.path).join("test.txt"))?,
"line1\nline2\nline3\nline4\n"
);
Ok(())
}
#[test]
fn unapply_branch() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case();
// create a commit and set the target
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
commit_all(ctx.repository());
set_test_target(ctx)?;
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nbranch1\n",
)?;
let file_path2 = Path::new("test2.txt");
std::fs::write(Path::new(&project.path).join(file_path2), "line5\nline6\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch2_id,
ownership: Some("test2.txt:1-3".parse()?),
..Default::default()
},
)?;
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
assert_eq!(
"line1\nline2\nline3\nline4\nbranch1\n",
String::from_utf8(contents)?
);
let contents = std::fs::read(Path::new(&project.path).join(file_path2))?;
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
assert!(branch.active);
let branch_manager = ctx.branch_manager();
let real_branch = branch_manager.save_and_unapply(branch1_id, guard.write_permission())?;
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
assert_eq!("line1\nline2\nline3\nline4\n", String::from_utf8(contents)?);
let contents = std::fs::read(Path::new(&project.path).join(file_path2))?;
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
assert!(!branches.iter().any(|b| b.id == branch1_id));
let branch_manager = ctx.branch_manager();
let branch1_id = branch_manager.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch)?,
None,
guard.write_permission(),
)?;
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
assert_eq!(
"line1\nline2\nline3\nline4\nbranch1\n",
String::from_utf8(contents)?
);
let contents = std::fs::read(Path::new(&project.path).join(file_path2))?;
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
// TODO: expect there to be 0 branches
assert_eq!(branch.files.len(), 0);
assert!(branch.active);
Ok(())
}
#[test]
fn apply_unapply_added_deleted_files() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case();
// create a commit and set the target
let file_path = Path::new("test.txt");
std::fs::write(Path::new(&project.path).join(file_path), "file1\n")?;
let file_path2 = Path::new("test2.txt");
std::fs::write(Path::new(&project.path).join(file_path2), "file2\n")?;
commit_all(ctx.repository());
set_test_target(ctx)?;
// rm file_path2, add file3
std::fs::remove_file(Path::new(&project.path).join(file_path2))?;
let file_path3 = Path::new("test3.txt");
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch3_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch2_id,
ownership: Some("test2.txt:0-0".parse()?),
..Default::default()
},
)?;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch3_id,
ownership: Some("test3.txt:1-2".parse()?),
..Default::default()
},
)?;
internal::list_virtual_branches(ctx, guard.write_permission()).unwrap();
let branch_manager = ctx.branch_manager();
let real_branch_2 = branch_manager.save_and_unapply(branch2_id, guard.write_permission())?;
// check that file2 is back
let contents = std::fs::read(Path::new(&project.path).join(file_path2))?;
assert_eq!("file2\n", String::from_utf8(contents)?);
let real_branch_3 = branch_manager.save_and_unapply(branch3_id, guard.write_permission())?;
// check that file3 is gone
assert!(!Path::new(&project.path).join(file_path3).exists());
branch_manager
.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch_2).unwrap(),
None,
guard.write_permission(),
)
.unwrap();
// check that file2 is gone
assert!(!Path::new(&project.path).join(file_path2).exists());
branch_manager
.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch_3).unwrap(),
None,
guard.write_permission(),
)
.unwrap();
// check that file3 is back
let contents = std::fs::read(Path::new(&project.path).join(file_path3))?;
assert_eq!("file3\n", String::from_utf8(contents)?);
Ok(())
}
// Verifies that we are able to detect when a remote branch is conflicting with the current applied branches.
#[test]
fn detect_mergeable_branch() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case();
// create a commit and set the target
let file_path = Path::new("test.txt");
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
commit_all(ctx.repository());
set_test_target(ctx)?;
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nbranch1\n",
)?;
let file_path4 = Path::new("test4.txt");
std::fs::write(Path::new(&project.path).join(file_path4), "line5\nline6\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch2_id,
ownership: Some("test4.txt:1-3".parse()?),
..Default::default()
},
)
.expect("failed to update branch");
// unapply both branches and create some conflicting ones
let branch_manager = ctx.branch_manager();
branch_manager.save_and_unapply(branch1_id, guard.write_permission())?;
branch_manager.save_and_unapply(branch2_id, guard.write_permission())?;
ctx.repository().set_head("refs/heads/master")?;
ctx.repository()
.checkout_head(Some(&mut git2::build::CheckoutBuilder::default().force()))?;
// create an upstream remote conflicting commit
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
commit_all(ctx.repository());
let up_target = ctx.repository().head().unwrap().target().unwrap();
ctx.repository().reference(
"refs/remotes/origin/remote_branch",
up_target,
true,
"update target",
)?;
// revert content and write a mergeable branch
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
let file_path3 = Path::new("test3.txt");
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
commit_all(ctx.repository());
let up_target = ctx.repository().head().unwrap().target().unwrap();
ctx.repository().reference(
"refs/remotes/origin/remote_branch2",
up_target,
true,
"update target",
)?;
// remove file_path3
std::fs::remove_file(Path::new(&project.path).join(file_path3))?;
ctx.repository()
.set_head("refs/heads/gitbutler/workspace")?;
ctx.repository()
.checkout_head(Some(&mut git2::build::CheckoutBuilder::default().force()))?;
// create branches that conflict with our earlier branches
let branch_manager = ctx.branch_manager();
branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch");
let branch4_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
// branch3 conflicts with branch1 and remote_branch
std::fs::write(
Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nbranch3\n",
)?;
// branch4 conflicts with branch2
let file_path2 = Path::new("test2.txt");
std::fs::write(
Path::new(&project.path).join(file_path2),
"line1\nline2\nline3\nline4\nbranch4\n",
)?;
let vb_state = VirtualBranchesHandle::new(project.gb_dir());
let mut branch4 = vb_state.get_branch_in_workspace(branch4_id)?;
branch4.ownership = BranchOwnershipClaims {
claims: vec!["test2.txt:1-6".parse()?],
};
vb_state.set_branch(branch4.clone())?;
let remotes = gitbutler_branch_actions::internal::list_local_branches(ctx)
.expect("failed to list remotes");
let _remote1 = &remotes
.iter()
.find(|b| b.name.to_string() == "refs/remotes/origin/remote_branch")
.unwrap();
assert!(!internal::is_remote_branch_mergeable(
ctx,
&"refs/remotes/origin/remote_branch".parse().unwrap()
)
.unwrap());
// assert_eq!(remote1.commits.len(), 1);
let _remote2 = &remotes
.iter()
.find(|b| b.name.to_string() == "refs/remotes/origin/remote_branch2")
.unwrap();
assert!(internal::is_remote_branch_mergeable(
ctx,
&"refs/remotes/origin/remote_branch2".parse().unwrap()
)
.unwrap());
// assert_eq!(remote2.commits.len(), 2);
Ok(())
}
#[test]
fn upstream_integrated_vbranch() -> Result<()> {
// ok, we need a vbranch with some work and an upstream target that also includes that work, but the base is behind
// plus a branch with work not in upstream so we can see that it is not included in the vbranch
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "file1\n"),
(PathBuf::from("test2.txt"), "file2\n"),
(PathBuf::from("test3.txt"), "file3\n"),
]));
let vb_state = VirtualBranchesHandle::new(project.gb_dir());
let base_commit = ctx.repository().head().unwrap().target().unwrap();
std::fs::write(
Path::new(&project.path).join("test.txt"),
"file1\nversion2\n",
)?;
commit_all(ctx.repository());
let upstream_commit = ctx.repository().head().unwrap().target().unwrap();
ctx.repository().reference(
"refs/remotes/origin/master",
upstream_commit,
true,
"update target",
)?;
vb_state.set_default_target(Target {
branch: "refs/remotes/origin/master".parse().unwrap(),
remote_url: "http://origin.com/project".to_string(),
sha: base_commit,
push_remote_name: None,
})?;
ctx.repository()
.remote("origin", "http://origin.com/project")?;
update_workspace_commit(&vb_state, ctx)?;
// create vbranches, one integrated, one not
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch2_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
let branch3_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test2.txt"),
"file2\nversion2\n",
)?;
std::fs::write(
Path::new(&project.path).join("test3.txt"),
"file3\nversion2\n",
)?;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch1_id,
name: Some("integrated".to_string()),
ownership: Some("test.txt:1-2".parse()?),
..Default::default()
},
)?;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch2_id,
name: Some("not integrated".to_string()),
ownership: Some("test2.txt:1-2".parse()?),
..Default::default()
},
)?;
internal::update_branch(
ctx,
&BranchUpdateRequest {
id: branch3_id,
name: Some("not committed".to_string()),
ownership: Some("test3.txt:1-2".parse()?),
..Default::default()
},
)?;
// create a new virtual branch from the remote branch
internal::commit(ctx, branch1_id, "integrated commit", None, false)?;
internal::commit(ctx, branch2_id, "non-integrated commit", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert!(branch1.commits.iter().any(|c| c.is_integrated));
assert_eq!(branch1.files.len(), 0);
assert_eq!(branch1.commits.len(), 1);
let branch2 = &branches.iter().find(|b| b.id == branch2_id).unwrap();
assert!(!branch2.commits.iter().any(|c| c.is_integrated));
assert_eq!(branch2.files.len(), 0);
assert_eq!(branch2.commits.len(), 1);
let branch3 = &branches.iter().find(|b| b.id == branch3_id).unwrap();
assert!(!branch3.commits.iter().any(|c| c.is_integrated));
assert_eq!(branch3.files.len(), 1);
assert_eq!(branch3.commits.len(), 0);
Ok(())
}
#[test]
fn commit_same_hunk_twice() -> Result<()> {
let suite = Suite::default();
let Case {
ctx,
project,
..
} = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].hunks.len(), 1);
assert_eq!(branch.commits.len(), 0);
// commit
internal::commit(ctx, branch1_id, "first commit to test.txt", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 0, "no files expected");
assert_eq!(branch.commits.len(), 1, "file should have been commited");
assert_eq!(branch.commits[0].files.len(), 1, "hunks expected");
assert_eq!(
branch.commits[0].files[0].hunks.len(),
1,
"one hunk should have been commited"
);
// update same lines
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line1\nPATCH1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1, "one file should be changed");
assert_eq!(branch.commits.len(), 1, "commit is still there");
internal::commit(ctx, branch1_id, "second commit to test.txt", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(
branch.files.len(),
0,
"all changes should have been commited"
);
assert_eq!(branch.commits.len(), 2, "two commits expected");
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(branch.commits[0].files[0].hunks.len(), 1);
assert_eq!(branch.commits[1].files.len(), 1);
assert_eq!(branch.commits[1].files[0].hunks.len(), 1);
Ok(())
}
#[test]
fn commit_same_file_twice() -> Result<()> {
let suite = Suite::default();
let Case {
ctx,
project,
..
} = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].hunks.len(), 1);
assert_eq!(branch.commits.len(), 0);
// commit
internal::commit(ctx, branch1_id, "first commit to test.txt", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 0, "no files expected");
assert_eq!(branch.commits.len(), 1, "file should have been commited");
assert_eq!(branch.commits[0].files.len(), 1, "hunks expected");
assert_eq!(
branch.commits[0].files[0].hunks.len(),
1,
"one hunk should have been commited"
);
// add second patch
std::fs::write(
Path::new(&project.path).join("file.txt"),
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1, "one file should be changed");
assert_eq!(branch.commits.len(), 1, "commit is still there");
internal::commit(ctx, branch1_id, "second commit to test.txt", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(
branch.files.len(),
0,
"all changes should have been commited"
);
assert_eq!(branch.commits.len(), 2, "two commits expected");
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(branch.commits[0].files[0].hunks.len(), 1);
assert_eq!(branch.commits[1].files.len(), 1);
assert_eq!(branch.commits[1].files[0].hunks.len(), 1);
Ok(())
}
#[test]
fn commit_partial_by_hunk() -> Result<()> {
let suite = Suite::default();
let Case {
ctx,
project,
..
} = &suite.new_case_with_files(HashMap::from([(
PathBuf::from("test.txt"),
"line1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
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(), 0);
// commit
internal::commit(
ctx,
branch1_id,
"first commit to test.txt",
Some(&"test.txt:1-6".parse::<BranchOwnershipClaims>().unwrap()),
false,
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].hunks.len(), 1);
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(branch.commits[0].files[0].hunks.len(), 1);
internal::commit(
ctx,
branch1_id,
"second commit to test.txt",
Some(&"test.txt:16-22".parse::<BranchOwnershipClaims>().unwrap()),
false,
)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits.len(), 2);
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(branch.commits[0].files[0].hunks.len(), 1);
assert_eq!(branch.commits[1].files.len(), 1);
assert_eq!(branch.commits[1].files[0].hunks.len(), 1);
Ok(())
}
#[test]
fn commit_partial_by_file() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "file1\n"),
(PathBuf::from("test2.txt"), "file2\n"),
]));
let commit1_oid = ctx.repository().head().unwrap().target().unwrap();
let commit1 = ctx.repository().find_commit(commit1_oid).unwrap();
set_test_target(ctx)?;
// remove file
std::fs::remove_file(Path::new(&project.path).join("test2.txt"))?;
// add new file
let file_path3 = Path::new("test3.txt");
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
// commit
internal::commit(ctx, branch1_id, "branch1 commit", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
// branch one test.txt has just the 1st and 3rd hunks applied
let commit2 = &branch1.commits[0].id;
let commit2 = ctx
.repository()
.find_commit(commit2.to_owned())
.expect("failed to get commit object");
let tree = commit1.tree().expect("failed to get tree");
let file_list = tree_to_file_list(ctx.repository(), &tree);
assert_eq!(file_list, vec!["test.txt", "test2.txt"]);
// get the tree
let tree = commit2.tree().expect("failed to get tree");
let file_list = tree_to_file_list(ctx.repository(), &tree);
assert_eq!(file_list, vec!["test.txt", "test3.txt"]);
Ok(())
}
#[test]
fn commit_add_and_delete_files() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "file1\n"),
(PathBuf::from("test2.txt"), "file2\n"),
]));
let commit1_oid = ctx.repository().head().unwrap().target().unwrap();
let commit1 = ctx.repository().find_commit(commit1_oid).unwrap();
set_test_target(ctx)?;
// remove file
std::fs::remove_file(Path::new(&project.path).join("test2.txt"))?;
// add new file
let file_path3 = Path::new("test3.txt");
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
// commit
internal::commit(ctx, branch1_id, "branch1 commit", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
// branch one test.txt has just the 1st and 3rd hunks applied
let commit2 = &branch1.commits[0].id;
let commit2 = ctx
.repository()
.find_commit(commit2.to_owned())
.expect("failed to get commit object");
let tree = commit1.tree().expect("failed to get tree");
let file_list = tree_to_file_list(ctx.repository(), &tree);
assert_eq!(file_list, vec!["test.txt", "test2.txt"]);
// get the tree
let tree = commit2.tree().expect("failed to get tree");
let file_list = tree_to_file_list(ctx.repository(), &tree);
assert_eq!(file_list, vec!["test.txt", "test3.txt"]);
Ok(())
}
#[test]
#[cfg(target_family = "unix")]
fn commit_executable_and_symlinks() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "file1\n"),
(PathBuf::from("test2.txt"), "file2\n"),
]));
set_test_target(ctx)?;
// add symlinked file
let file_path3 = Path::new("test3.txt");
let src = Path::new(&project.path).join("test2.txt");
let dst = Path::new(&project.path).join(file_path3);
symlink(src, dst)?;
// add executable
let file_path4 = Path::new("test4.bin");
let exec = Path::new(&project.path).join(file_path4);
std::fs::write(&exec, "exec\n")?;
let permissions = std::fs::metadata(&exec)?.permissions();
let new_permissions = Permissions::from_mode(permissions.mode() | 0o111); // Add execute permission
std::fs::set_permissions(&exec, new_permissions)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
// commit
internal::commit(ctx, branch1_id, "branch1 commit", None, false)?;
let (branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
let commit = &branch1.commits[0].id;
let commit = ctx
.repository()
.find_commit(commit.to_owned())
.expect("failed to get commit object");
let tree = commit.tree().expect("failed to get tree");
let list = tree_to_entry_list(ctx.repository(), &tree);
assert_eq!(list[0].0, "test.txt");
assert_eq!(list[0].1, "100644");
assert_eq!(list[1].0, "test2.txt");
assert_eq!(list[1].1, "100644");
assert_eq!(list[2].0, "test3.txt");
assert_eq!(list[2].1, "120000");
assert_eq!(list[2].2, "test2.txt");
assert_eq!(list[3].0, "test4.bin");
assert_eq!(list[3].1, "100755");
Ok(())
}
fn tree_to_file_list(repository: &git2::Repository, tree: &git2::Tree) -> Vec<String> {
let mut file_list = Vec::new();
walk(tree, |_, entry| {
let path = entry.name().unwrap();
let entry = tree.get_path(Path::new(path)).unwrap();
let object = entry.to_object(repository).unwrap();
if object.kind() == Some(git2::ObjectType::Blob) {
file_list.push(path.to_string());
}
TreeWalkResult::Continue
})
.expect("failed to walk tree");
file_list
}
fn tree_to_entry_list(
repository: &git2::Repository,
tree: &git2::Tree,
) -> Vec<(String, String, String, String)> {
let mut file_list = Vec::new();
walk(tree, |_root, entry| {
let path = entry.name().unwrap();
let entry = tree.get_path(Path::new(path)).unwrap();
let object = entry.to_object(repository).unwrap();
let blob = object.as_blob().expect("failed to get blob");
// convert content to string
let octal_mode = format!("{:o}", entry.filemode());
if let Ok(content) =
std::str::from_utf8(blob.content()).context("failed to convert content to string")
{
file_list.push((
path.to_string(),
octal_mode,
content.to_string(),
blob.id().to_string(),
));
} else {
file_list.push((
path.to_string(),
octal_mode,
"BINARY".to_string(),
blob.id().to_string(),
));
}
TreeWalkResult::Continue
})
.expect("failed to walk tree");
file_list
}
#[test]
fn verify_branch_commits_to_workspace() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
set_test_target(ctx)?;
let mut guard = project.exclusive_worktree_access();
verify_branch(ctx, guard.write_permission()).unwrap();
// write two commits
let file_path2 = Path::new("test2.txt");
std::fs::write(Path::new(&project.path).join(file_path2), "file")?;
commit_all(ctx.repository());
std::fs::write(Path::new(&project.path).join(file_path2), "update")?;
commit_all(ctx.repository());
// verify puts commits onto the virtual branch
verify_branch(ctx, guard.write_permission()).unwrap();
// one virtual branch with two commits was created
let (virtual_branches, _) = internal::list_virtual_branches(ctx, guard.write_permission())?;
assert_eq!(virtual_branches.len(), 1);
let branch = &virtual_branches.first().unwrap();
assert_eq!(branch.commits.len(), 2);
assert_eq!(branch.commits.len(), 2);
Ok(())
}
#[test]
fn verify_branch_not_workspace() -> Result<()> {
let suite = Suite::default();
let Case { ctx, project, .. } = &suite.new_case();
set_test_target(ctx)?;
let mut guard = project.exclusive_worktree_access();
verify_branch(ctx, guard.write_permission()).unwrap();
ctx.repository().set_head("refs/heads/master")?;
let verify_result = verify_branch(ctx, guard.write_permission());
assert!(verify_result.is_err());
assert_eq!(
format!("{:#}", verify_result.unwrap_err()),
"<verification-failed>: project is on refs/heads/master. Please checkout gitbutler/workspace to continue"
);
Ok(())
}
#[test]
fn pre_commit_hook_rejection() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "line1\nline2\nline3\nline4\n"),
(PathBuf::from("test2.txt"), "line5\nline6\nline7\nline8\n"),
]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\n",
)?;
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
git2_hooks::create_hook(ctx.repository(), git2_hooks::HOOK_PRE_COMMIT, hook);
let res = internal::commit(ctx, branch1_id, "test commit", None, true);
let err = res.unwrap_err();
assert_eq!(
err.source().unwrap().to_string(),
"commit hook rejected: rejected"
);
Ok(())
}
#[test]
fn post_commit_hook() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "line1\nline2\nline3\nline4\n"),
(PathBuf::from("test2.txt"), "line5\nline6\nline7\nline8\n"),
]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\n",
)?;
let hook = b"#!/bin/sh
touch hook_ran
";
git2_hooks::create_hook(ctx.repository(), git2_hooks::HOOK_POST_COMMIT, hook);
let hook_ran_proof = ctx.repository().path().parent().unwrap().join("hook_ran");
assert!(!hook_ran_proof.exists());
internal::commit(ctx, branch1_id, "test commit", None, true)?;
assert!(hook_ran_proof.exists());
Ok(())
}
#[test]
fn commit_msg_hook_rejection() -> Result<()> {
let suite = Suite::default();
let Case { project, ctx, .. } = &suite.new_case_with_files(HashMap::from([
(PathBuf::from("test.txt"), "line1\nline2\nline3\nline4\n"),
(PathBuf::from("test2.txt"), "line5\nline6\nline7\nline8\n"),
]));
set_test_target(ctx)?;
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
let branch1_id = branch_manager
.create_virtual_branch(&BranchCreateRequest::default(), guard.write_permission())
.expect("failed to create virtual branch")
.id;
std::fs::write(
Path::new(&project.path).join("test.txt"),
"line0\nline1\nline2\nline3\nline4\n",
)?;
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
git2_hooks::create_hook(ctx.repository(), git2_hooks::HOOK_COMMIT_MSG, hook);
let res = internal::commit(ctx, branch1_id, "test commit", None, true);
let err = res.unwrap_err();
assert_eq!(
err.source().unwrap().to_string(),
"commit-msg hook rejected: rejected"
);
Ok(())
}
fn walk<C>(tree: &git2::Tree, mut callback: C) -> Result<()>
where
C: FnMut(&str, &TreeEntry) -> TreeWalkResult,
{
tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
match callback(root, &entry.clone()) {
TreeWalkResult::Continue => git2::TreeWalkResult::Ok,
}
})
.map_err(Into::into)
}
enum TreeWalkResult {
Continue,
}