mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-30 04:58:55 +03:00
c8658b9a23
This is to establish a patter of which crates represent a higher level logic
420 lines
12 KiB
Rust
420 lines
12 KiB
Rust
use super::*;
|
|
use gitbutler_branchstate::VirtualBranchesExt;
|
|
use gitbutler_oplog::oplog::Oplog;
|
|
use itertools::Itertools;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
#[tokio::test]
|
|
async fn workdir_vbranch_restore() -> anyhow::Result<()> {
|
|
let test = Test::default();
|
|
let Test {
|
|
repository,
|
|
controller,
|
|
project,
|
|
..
|
|
} = &test;
|
|
|
|
controller
|
|
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
|
.await
|
|
.unwrap();
|
|
|
|
let worktree_dir = repository.path();
|
|
for round in 0..3 {
|
|
let line_count = round * 20;
|
|
fs::write(
|
|
worktree_dir.join(format!("file{round}.txt")),
|
|
make_lines(line_count),
|
|
)?;
|
|
let branch_id = controller
|
|
.create_virtual_branch(
|
|
project,
|
|
&branch::BranchCreateRequest {
|
|
name: Some(round.to_string()),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await?;
|
|
controller
|
|
.create_commit(
|
|
project,
|
|
branch_id,
|
|
&format!("commit {round}"),
|
|
None,
|
|
false, /* run hook */
|
|
)
|
|
.await?;
|
|
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
|
|
);
|
|
}
|
|
let _empty = controller
|
|
.create_virtual_branch(project, &Default::default())
|
|
.await?;
|
|
|
|
let snapshots = project.list_snapshots(10, None)?;
|
|
assert_eq!(
|
|
snapshots.len(),
|
|
7,
|
|
"3 vbranches + 3 commits + one empty branch"
|
|
);
|
|
|
|
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");
|
|
|
|
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"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
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> {
|
|
(0..count).map(|n| n.to_string()).join("\n").into()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn basic_oplog() -> anyhow::Result<()> {
|
|
let Test {
|
|
repository,
|
|
controller,
|
|
project,
|
|
..
|
|
} = &Test::default();
|
|
|
|
controller
|
|
.set_base_branch(project, &"refs/remotes/origin/master".parse()?)
|
|
.await?;
|
|
|
|
let branch_id = controller
|
|
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
|
|
.await?;
|
|
|
|
// create commit
|
|
fs::write(repository.path().join("file.txt"), "content")?;
|
|
let _commit1_id = controller
|
|
.create_commit(project, branch_id, "commit one", None, false)
|
|
.await?;
|
|
|
|
// dont store large files
|
|
let file_path = repository.path().join("large.txt");
|
|
// write 33MB of random data in the file
|
|
let mut file = std::fs::File::create(file_path)?;
|
|
for _ in 0..33 * 1024 {
|
|
let data = [0u8; 1024];
|
|
file.write_all(&data)?;
|
|
}
|
|
|
|
// create commit with large file
|
|
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)
|
|
.await?;
|
|
|
|
// Create conflict state
|
|
let conflicts_path = repository.path().join(".git").join("conflicts");
|
|
std::fs::write(&conflicts_path, "conflict A")?;
|
|
let base_merge_parent_path = repository.path().join(".git").join("base_merge_parent");
|
|
std::fs::write(&base_merge_parent_path, "parent A")?;
|
|
|
|
// create state with conflict state
|
|
let _empty_branch_id = controller
|
|
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
|
|
.await?;
|
|
|
|
std::fs::remove_file(&base_merge_parent_path)?;
|
|
std::fs::remove_file(&conflicts_path)?;
|
|
|
|
fs::write(repository.path().join("file4.txt"), "content4")?;
|
|
let _commit3_id = controller
|
|
.create_commit(project, branch_id, "commit three", None, false)
|
|
.await?;
|
|
|
|
let branch = controller
|
|
.list_virtual_branches(project)
|
|
.await?
|
|
.0
|
|
.into_iter()
|
|
.find(|b| b.id == branch_id)
|
|
.unwrap();
|
|
|
|
let branches = controller.list_virtual_branches(project).await?;
|
|
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);
|
|
|
|
let snapshots = project.list_snapshots(10, None)?;
|
|
|
|
let ops = snapshots
|
|
.iter()
|
|
.map(|c| &c.details.as_ref().unwrap().title)
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
ops,
|
|
vec![
|
|
"CreateCommit",
|
|
"CreateBranch",
|
|
"CreateCommit",
|
|
"CreateCommit",
|
|
"CreateBranch",
|
|
]
|
|
);
|
|
|
|
project.restore_snapshot(snapshots[1].clone().commit_id)?;
|
|
|
|
// restores the conflict files
|
|
let file_lines = std::fs::read_to_string(&conflicts_path)?;
|
|
assert_eq!(file_lines, "conflict A");
|
|
let file_lines = std::fs::read_to_string(&base_merge_parent_path)?;
|
|
assert_eq!(file_lines, "parent A");
|
|
|
|
assert_eq!(snapshots[1].lines_added, 2);
|
|
assert_eq!(snapshots[1].lines_removed, 0);
|
|
|
|
project.restore_snapshot(snapshots[2].clone().commit_id)?;
|
|
|
|
// the restore removed our new branch
|
|
let branches = controller.list_virtual_branches(project).await?;
|
|
assert_eq!(branches.0.len(), 1);
|
|
|
|
// assert that the conflicts file was removed
|
|
assert!(!&conflicts_path.try_exists()?);
|
|
|
|
// remove commit2_oid from odb
|
|
let commit_str = &commit2_id.to_string();
|
|
// find file in odb
|
|
let file_path = repository
|
|
.path()
|
|
.join(".git")
|
|
.join("objects")
|
|
.join(&commit_str[..2]);
|
|
let file_path = file_path.join(&commit_str[2..]);
|
|
assert!(file_path.exists());
|
|
// remove file
|
|
std::fs::remove_file(file_path)?;
|
|
|
|
// try to look up that object
|
|
let repo = git2::Repository::open(&project.path)?;
|
|
let commit = repo.find_commit(commit2_id);
|
|
assert!(commit.is_err());
|
|
|
|
project.restore_snapshot(snapshots[1].clone().commit_id)?;
|
|
|
|
// test missing commits are recreated
|
|
let commit = repo.find_commit(commit2_id);
|
|
assert!(commit.is_ok());
|
|
|
|
let file_path = repository.path().join("large.txt");
|
|
assert!(file_path.exists());
|
|
|
|
let file_path = repository.path().join("file.txt");
|
|
let file_lines = std::fs::read_to_string(file_path)?;
|
|
assert_eq!(file_lines, "content");
|
|
|
|
assert!(
|
|
!project.should_auto_snapshot(Duration::ZERO)?,
|
|
"not enough lines changed"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
|
let Test {
|
|
repository,
|
|
controller,
|
|
project,
|
|
..
|
|
} = &Test::default();
|
|
|
|
controller
|
|
.set_base_branch(project, &"refs/remotes/origin/master".parse()?)
|
|
.await?;
|
|
|
|
assert_eq!(
|
|
project
|
|
.virtual_branches()
|
|
.list_branches_in_workspace()?
|
|
.len(),
|
|
0
|
|
);
|
|
let branch_id = controller
|
|
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
|
|
.await?;
|
|
assert_eq!(
|
|
project
|
|
.virtual_branches()
|
|
.list_branches_in_workspace()?
|
|
.len(),
|
|
1
|
|
);
|
|
|
|
// create commit
|
|
fs::write(repository.path().join("file.txt"), "content")?;
|
|
let _commit1_id = controller
|
|
.create_commit(project, branch_id, "commit one", None, false)
|
|
.await?;
|
|
|
|
let repo = git2::Repository::open(&project.path)?;
|
|
|
|
// check the integration commit
|
|
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
|
|
fs::write(repository.path().join("file.txt"), "changed content")?;
|
|
let _commit2_id = controller
|
|
.create_commit(project, branch_id, "commit two", None, false)
|
|
.await?;
|
|
|
|
// check the integration commit changed
|
|
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");
|
|
assert_ne!(commit1_id, commit2_id);
|
|
|
|
// restore the first
|
|
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 = project.virtual_branches().list_branches_in_workspace()?;
|
|
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..],
|
|
);
|
|
|
|
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(())
|
|
}
|
|
|
|
// test operations-log.toml head is not a commit
|
|
#[tokio::test]
|
|
async fn head_corrupt_is_recreated_automatically() {
|
|
let Test {
|
|
repository,
|
|
controller,
|
|
project,
|
|
..
|
|
} = &Test::default();
|
|
|
|
controller
|
|
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
|
.await
|
|
.unwrap();
|
|
controller
|
|
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
|
.await
|
|
.unwrap();
|
|
|
|
let snapshots = project.list_snapshots(10, None).unwrap();
|
|
assert_eq!(
|
|
snapshots.len(),
|
|
1,
|
|
"No snapshots can be created before a base branch is set, hence only 1 snapshot despite two calls"
|
|
);
|
|
|
|
// overwrite oplog head with a non-commit sha
|
|
let oplog_path = repository.path().join(".git/gitbutler/operations-log.toml");
|
|
fs::write(
|
|
oplog_path,
|
|
"head_sha = \"758d54f587227fba3da3b61fbb54a99c17903d59\"",
|
|
)
|
|
.unwrap();
|
|
|
|
controller
|
|
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
|
.await
|
|
.expect("the snapshot doesn't fail despite the corrupt head");
|
|
|
|
let snapshots = project.list_snapshots(10, None).unwrap();
|
|
assert_eq!(
|
|
snapshots.len(),
|
|
1,
|
|
"it should have just reset the oplog head, so only 1, not 2"
|
|
);
|
|
}
|