gitbutler/crates/gitbutler-branch-actions/tests/virtual_branches/oplog.rs

388 lines
12 KiB
Rust
Raw Normal View History

use std::{io::Write, path::Path, time::Duration};
use gitbutler_branch::{BranchCreateRequest, VirtualBranchesHandle};
use gitbutler_oplog::OplogExt;
2024-05-27 16:23:04 +03:00
use itertools::Itertools;
use super::*;
2024-05-17 16:11:09 +03:00
#[test]
fn workdir_vbranch_restore() -> anyhow::Result<()> {
2024-05-28 19:54:50 +03:00
let test = Test::default();
2024-05-17 16:11:09 +03:00
let Test {
repository,
controller,
project,
..
2024-05-28 19:54:50 +03:00
} = &test;
2024-05-17 16:11:09 +03:00
controller
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
2024-05-17 16:11:09 +03:00
.unwrap();
2024-05-28 19:54:50 +03:00
let worktree_dir = repository.path();
2024-05-27 16:23:04 +03:00
for round in 0..3 {
2024-05-28 19:54:50 +03:00
let line_count = round * 20;
2024-05-27 16:23:04 +03:00
fs::write(
2024-05-28 19:54:50 +03:00
worktree_dir.join(format!("file{round}.txt")),
2024-07-02 16:11:28 +03:00
make_lines(line_count),
2024-05-27 16:23:04 +03:00
)?;
let branch_id = controller.create_virtual_branch(
project,
&BranchCreateRequest {
name: Some(round.to_string()),
..Default::default()
},
)?;
controller.create_commit(
project,
branch_id,
&format!("commit {round}"),
None,
false, /* run hook */
)?;
2024-05-28 19:54:50 +03:00
assert_eq!(
wd_file_count(&worktree_dir)?,
round + 1,
"each round creates a new file, and it persists"
);
assert_eq!(
project.should_auto_snapshot(Duration::ZERO)?,
line_count > 20
);
2024-05-27 16:23:04 +03:00
}
let _empty = controller.create_virtual_branch(project, &Default::default())?;
2024-05-27 16:23:04 +03:00
2024-05-28 19:54:50 +03:00
let snapshots = project.list_snapshots(10, None)?;
2024-05-27 16:23:04 +03:00
assert_eq!(
2024-05-28 19:54:50 +03:00
snapshots.len(),
7,
"3 vbranches + 3 commits + one empty branch"
2024-05-27 16:23:04 +03:00
);
2024-05-28 19:54:50 +03:00
let previous_files_count = wd_file_count(&worktree_dir)?;
assert_eq!(previous_files_count, 3, "one file per round");
project
.restore_snapshot(snapshots[0].commit_id)
.expect("restoration succeeds");
2024-05-27 16:23:04 +03:00
2024-05-28 19:54:50 +03:00
assert_eq!(
project.list_snapshots(10, None)?.len(),
8,
"all the previous + 1 restore commit"
);
let current_files = wd_file_count(&worktree_dir)?;
assert_eq!(
current_files, previous_files_count,
"we only removed an empty vbranch, no worktree change"
);
assert!(
!project.should_auto_snapshot(Duration::ZERO)?,
"not enough lines changed"
);
2024-05-27 16:23:04 +03:00
Ok(())
}
2024-05-28 19:54:50 +03:00
fn wd_file_count(worktree_dir: &&Path) -> anyhow::Result<usize> {
Ok(glob::glob(&worktree_dir.join("file*").to_string_lossy())?.count())
}
fn make_lines(count: usize) -> Vec<u8> {
2024-05-27 16:23:04 +03:00
(0..count).map(|n| n.to_string()).join("\n").into()
}
#[test]
fn basic_oplog() -> anyhow::Result<()> {
2024-05-27 16:23:04 +03:00
let Test {
repository,
controller,
project,
..
} = &Test::default();
controller.set_base_branch(project, &"refs/remotes/origin/master".parse()?)?;
2024-05-27 16:23:04 +03:00
let branch_id = controller.create_virtual_branch(project, &BranchCreateRequest::default())?;
2024-05-17 16:11:09 +03:00
// create commit
2024-05-27 16:23:04 +03:00
fs::write(repository.path().join("file.txt"), "content")?;
let _commit1_id = controller.create_commit(project, branch_id, "commit one", None, false)?;
2024-05-17 16:11:09 +03:00
// dont store large files
let file_path = repository.path().join("large.txt");
// write 33MB of random data in the file
2024-05-27 16:23:04 +03:00
let mut file = std::fs::File::create(file_path)?;
2024-05-17 16:11:09 +03:00
for _ in 0..33 * 1024 {
let data = [0u8; 1024];
2024-05-27 16:23:04 +03:00
file.write_all(&data)?;
2024-05-17 16:11:09 +03:00
}
// create commit with large file
2024-05-27 16:23:04 +03:00
fs::write(repository.path().join("file2.txt"), "content2")?;
fs::write(repository.path().join("file3.txt"), "content3")?;
let commit2_id = controller.create_commit(project, branch_id, "commit two", None, false)?;
2024-05-17 16:11:09 +03:00
// Create conflict state
let conflicts_path = repository.path().join(".git").join("conflicts");
2024-05-27 16:23:04 +03:00
std::fs::write(&conflicts_path, "conflict A")?;
2024-05-17 16:11:09 +03:00
let base_merge_parent_path = repository.path().join(".git").join("base_merge_parent");
2024-05-27 16:23:04 +03:00
std::fs::write(&base_merge_parent_path, "parent A")?;
2024-05-17 16:11:09 +03:00
// create state with conflict state
let _empty_branch_id =
controller.create_virtual_branch(project, &BranchCreateRequest::default())?;
2024-05-17 16:11:09 +03:00
2024-05-27 16:23:04 +03:00
std::fs::remove_file(&base_merge_parent_path)?;
std::fs::remove_file(&conflicts_path)?;
2024-05-17 16:11:09 +03:00
2024-05-27 16:23:04 +03:00
fs::write(repository.path().join("file4.txt"), "content4")?;
let _commit3_id = controller.create_commit(project, branch_id, "commit three", None, false)?;
2024-05-17 16:11:09 +03:00
let branch = controller
.list_virtual_branches(project)?
2024-05-17 16:11:09 +03:00
.0
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let branches = controller.list_virtual_branches(project)?;
2024-05-17 16:11:09 +03:00
assert_eq!(branches.0.len(), 2);
assert_eq!(branch.commits.len(), 3);
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(branch.commits[1].files.len(), 3);
2024-05-27 16:23:04 +03:00
let snapshots = project.list_snapshots(10, None)?;
2024-05-17 16:11:09 +03:00
let ops = snapshots
.iter()
.map(|c| &c.details.as_ref().unwrap().title)
.collect::<Vec<_>>();
assert_eq!(
ops,
vec![
"CreateCommit",
"CreateBranch",
"CreateCommit",
"CreateCommit",
"CreateBranch",
]
);
2024-05-27 16:23:04 +03:00
project.restore_snapshot(snapshots[1].clone().commit_id)?;
2024-05-17 16:11:09 +03:00
// restores the conflict files
2024-05-27 16:23:04 +03:00
let file_lines = std::fs::read_to_string(&conflicts_path)?;
2024-05-17 16:11:09 +03:00
assert_eq!(file_lines, "conflict A");
2024-05-27 16:23:04 +03:00
let file_lines = std::fs::read_to_string(&base_merge_parent_path)?;
2024-05-17 16:11:09 +03:00
assert_eq!(file_lines, "parent A");
2024-05-23 14:40:53 +03:00
assert_eq!(snapshots[1].lines_added, 2);
assert_eq!(snapshots[1].lines_removed, 0);
2024-05-17 16:11:09 +03:00
2024-05-27 16:23:04 +03:00
project.restore_snapshot(snapshots[2].clone().commit_id)?;
2024-05-17 16:11:09 +03:00
// the restore removed our new branch
let branches = controller.list_virtual_branches(project)?;
2024-05-17 16:11:09 +03:00
assert_eq!(branches.0.len(), 1);
2024-05-17 16:16:04 +03:00
2024-05-17 16:11:09 +03:00
// assert that the conflicts file was removed
2024-05-27 16:23:04 +03:00
assert!(!&conflicts_path.try_exists()?);
2024-05-17 16:11:09 +03:00
// remove commit2_oid from odb
let commit_str = &commit2_id.to_string();
// find file in odb
2024-05-17 16:16:04 +03:00
let file_path = repository
.path()
.join(".git")
.join("objects")
.join(&commit_str[..2]);
2024-05-17 16:11:09 +03:00
let file_path = file_path.join(&commit_str[2..]);
assert!(file_path.exists());
// remove file
2024-05-27 16:23:04 +03:00
std::fs::remove_file(file_path)?;
2024-05-17 16:11:09 +03:00
// try to look up that object
2024-05-27 16:23:04 +03:00
let repo = git2::Repository::open(&project.path)?;
2024-06-05 23:56:03 +03:00
let commit = repo.find_commit(commit2_id);
2024-05-17 16:11:09 +03:00
assert!(commit.is_err());
2024-05-27 16:23:04 +03:00
project.restore_snapshot(snapshots[1].clone().commit_id)?;
2024-05-17 16:11:09 +03:00
// test missing commits are recreated
2024-06-05 23:56:03 +03:00
let commit = repo.find_commit(commit2_id);
2024-05-17 16:11:09 +03:00
assert!(commit.is_ok());
let file_path = repository.path().join("large.txt");
assert!(file_path.exists());
let file_path = repository.path().join("file.txt");
2024-05-27 16:23:04 +03:00
let file_lines = std::fs::read_to_string(file_path)?;
2024-05-17 16:11:09 +03:00
assert_eq!(file_lines, "content");
2024-05-27 16:23:04 +03:00
assert!(
!project.should_auto_snapshot(Duration::ZERO)?,
"not enough lines changed"
);
Ok(())
2024-05-17 16:11:09 +03:00
}
#[test]
fn restores_gitbutler_integration() -> anyhow::Result<()> {
let Test {
repository,
controller,
project,
..
} = &Test::default();
controller.set_base_branch(project, &"refs/remotes/origin/master".parse()?)?;
assert_eq!(
VirtualBranchesHandle::new(project.gb_dir())
.list_branches_in_workspace()?
.len(),
0
);
let branch_id = controller.create_virtual_branch(project, &BranchCreateRequest::default())?;
assert_eq!(
VirtualBranchesHandle::new(project.gb_dir())
.list_branches_in_workspace()?
.len(),
1
);
// create commit
2024-05-27 16:23:04 +03:00
fs::write(repository.path().join("file.txt"), "content")?;
let _commit1_id = controller.create_commit(project, branch_id, "commit one", None, false)?;
2024-05-27 16:23:04 +03:00
let repo = git2::Repository::open(&project.path)?;
// check the integration commit
2024-05-27 16:23:04 +03:00
let head = repo.head().expect("never unborn");
let commit = &head.peel_to_commit()?;
let commit1_id = commit.id();
let message = commit.summary().unwrap();
assert_eq!(message, "GitButler Integration Commit");
// create second commit
2024-05-27 16:23:04 +03:00
fs::write(repository.path().join("file.txt"), "changed content")?;
let _commit2_id = controller.create_commit(project, branch_id, "commit two", None, false)?;
// check the integration commit changed
2024-05-27 16:23:04 +03:00
let head = repo.head().expect("never unborn");
let commit = &head.peel_to_commit()?;
let commit2_id = commit.id();
let message = commit.summary().unwrap();
assert_eq!(message, "GitButler Integration Commit");
2024-05-27 16:23:04 +03:00
assert_ne!(commit1_id, commit2_id);
// restore the first
2024-05-27 16:23:04 +03:00
let snapshots = project.list_snapshots(10, None)?;
assert_eq!(
snapshots.len(),
3,
"one vbranch, two commits, one snapshot each"
);
project
.restore_snapshot(snapshots[0].commit_id)
.expect("can restore the most recent snapshot, to undo commit 2, resetting to commit 1");
let head = repo.head().expect("never unborn");
let current_commit = &head.peel_to_commit()?;
let id_of_restored_commit = current_commit.id();
assert_eq!(
commit1_id, id_of_restored_commit,
"head now points to the first commit, it's not commit 2 anymore"
);
let vbranches = VirtualBranchesHandle::new(project.gb_dir()).list_branches_in_workspace()?;
2024-05-27 16:23:04 +03:00
assert_eq!(
vbranches.len(),
1,
"vbranches aren't affected by this (only the head commit)"
);
let all_snapshots = project.list_snapshots(10, None)?;
assert_eq!(
all_snapshots.len(),
4,
"the restore is tracked as separate snapshot"
);
assert_eq!(
project.list_snapshots(0, None)?.len(),
0,
"it respects even non-sensical limits"
);
let snapshots = project.list_snapshots(1, None)?;
assert_eq!(snapshots.len(), 1);
assert_eq!(
project.list_snapshots(1, Some(snapshots[0].commit_id))?,
snapshots,
"traversal from oplog head is the same as if it wasn't specified, and the given head is returned first"
);
assert_eq!(
project.list_snapshots(10, Some(all_snapshots[2].commit_id))?,
&all_snapshots[2..],
);
2024-05-27 16:23:04 +03:00
let first_snapshot = all_snapshots.last().unwrap();
assert_eq!(
(
first_snapshot.lines_added,
first_snapshot.lines_removed,
first_snapshot.files_changed.len()
),
(0, 0, 0),
"The first snapshot is intentionally not listing everything as changed"
);
Ok(())
}
2024-05-17 16:11:09 +03:00
// test operations-log.toml head is not a commit
#[test]
fn head_corrupt_is_recreated_automatically() {
2024-05-17 16:11:09 +03:00
let Test {
repository,
controller,
project,
..
} = &Test::default();
2024-05-23 14:40:53 +03:00
controller
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
2024-05-23 14:40:53 +03:00
.unwrap();
2024-05-17 16:11:09 +03:00
controller
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
2024-05-17 16:11:09 +03:00
.unwrap();
let snapshots = project.list_snapshots(10, None).unwrap();
2024-05-27 16:23:04 +03:00
assert_eq!(
snapshots.len(),
1,
"No snapshots can be created before a base branch is set, hence only 1 snapshot despite two calls"
);
2024-05-17 16:11:09 +03:00
// overwrite oplog head with a non-commit sha
2024-05-27 16:23:04 +03:00
let oplog_path = repository.path().join(".git/gitbutler/operations-log.toml");
2024-05-17 16:16:04 +03:00
fs::write(
2024-05-27 16:23:04 +03:00
oplog_path,
2024-05-17 16:16:04 +03:00
"head_sha = \"758d54f587227fba3da3b61fbb54a99c17903d59\"",
)
.unwrap();
2024-05-17 16:11:09 +03:00
controller
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
2024-05-27 16:23:04 +03:00
.expect("the snapshot doesn't fail despite the corrupt head");
2024-05-17 16:11:09 +03:00
let snapshots = project.list_snapshots(10, None).unwrap();
2024-05-27 16:23:04 +03:00
assert_eq!(
snapshots.len(),
1,
"it should have just reset the oplog head, so only 1, not 2"
);
2024-05-17 16:11:09 +03:00
}