mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 10:33:21 +03:00
5b7109e8ee
This allows us to control the head setting and update the stack `heads` field accordingly
2165 lines
71 KiB
Rust
2165 lines
71 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 bstr::ByteSlice;
|
|
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(())
|
|
}
|
|
|
|
/// This sets up the following scenario:
|
|
///
|
|
/// Target commit:
|
|
/// test.txt: line1\nline2\nline3\nline4\n
|
|
///
|
|
/// Make commit "last push":
|
|
/// test.txt: line1\nline2\nline3\nline4\nupstream\n
|
|
///
|
|
/// "Server side" origin/master:
|
|
/// test.txt: line1\nline2\nline3\nline4\nupstream\ncoworker work\n
|
|
///
|
|
/// Write uncommited:
|
|
/// test.txt: line1\nline2\nline3\nline4\nupstream\n
|
|
/// test2.txt: file2\n
|
|
///
|
|
/// Create vbranch:
|
|
/// - set head to "last push"
|
|
///
|
|
/// Inspect Virtual branch:
|
|
/// commited: test.txt: line1\nline2\nline3\nline4\n+upstream\n
|
|
/// uncommited: test2.txt: file2\n
|
|
#[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.set_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,
|
|
"test2.txt contains uncommited changes"
|
|
);
|
|
assert_eq!(branch1.files[0].path.to_str().unwrap(), "test2.txt");
|
|
assert_eq!(
|
|
branch1.files[0].hunks[0].diff.to_str().unwrap(),
|
|
"@@ -0,0 +1 @@\n+file2\n"
|
|
);
|
|
|
|
assert_eq!(
|
|
branch1.commits.len(),
|
|
1,
|
|
"test.txt is commited inside this commit"
|
|
);
|
|
assert_eq!(branch1.commits[0].files.len(), 1);
|
|
assert_eq!(
|
|
branch1.commits[0].files[0].path.to_str().unwrap(),
|
|
"test.txt"
|
|
);
|
|
assert_eq!(
|
|
branch1.commits[0].files[0].hunks[0].diff.to_str().unwrap(),
|
|
"@@ -2,3 +2,4 @@ line1\n line2\n line3\n line4\n+upstream\n"
|
|
);
|
|
// 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.set_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();
|
|
assert_eq!(branch.files.len(), 1);
|
|
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,
|
|
}
|