Add a new testsupport crate that contains core/tests/shared.

It's code shared by multiple crates, and should be reusable
by means of a crate.
This commit is contained in:
Sebastian Thiel 2024-04-06 10:03:07 +02:00
parent 59e441a2eb
commit 3a148a556f
No known key found for this signature in database
GPG Key ID: 9CB5EE7895E8268B
6 changed files with 678 additions and 0 deletions

12
Cargo.lock generated
View File

@ -2129,6 +2129,18 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "gitbutler-testsupport"
version = "0.0.0"
dependencies = [
"anyhow",
"git2",
"gitbutler-core",
"once_cell",
"pretty_assertions",
"tempfile",
]
[[package]]
name = "glib"
version = "0.15.12"

View File

@ -4,6 +4,7 @@ members = [
"crates/gitbutler-tauri",
"crates/gitbutler-changeset",
"crates/gitbutler-git",
"crates/gitbutler-testsupport",
]
resolver = "2"

View File

@ -0,0 +1,18 @@
[package]
name = "gitbutler-testsupport"
version = "0.0.0"
edition = "2021"
publish = false
[lib]
path = "src/lib.rs"
doctest = false
test = false
[dependencies]
anyhow = "1.0.81"
once_cell = "1.19"
git2.workspace = true
pretty_assertions = "1.4"
tempfile = "3.10.1"
gitbutler-core = { path = "../gitbutler-core" }

View File

@ -0,0 +1,69 @@
pub const VAR_NO_CLEANUP: &str = "GITBUTLER_TESTS_NO_CLEANUP";
mod test_project;
pub use test_project::TestProject;
mod suite;
pub use suite::*;
pub mod paths {
use tempfile::TempDir;
use super::temp_dir;
pub fn data_dir() -> TempDir {
temp_dir()
}
}
pub mod virtual_branches {
use gitbutler_core::{
gb_repository, project_repository,
virtual_branches::{self, VirtualBranchesHandle},
};
use crate::empty_bare_repository;
pub fn set_test_target(
gb_repo: &gb_repository::Repository,
project_repository: &project_repository::Repository,
) -> anyhow::Result<()> {
let (remote_repo, _tmp) = empty_bare_repository();
let mut remote = project_repository
.git_repository
.remote(
"origin",
&remote_repo.path().to_str().unwrap().parse().unwrap(),
)
.expect("failed to add remote");
remote.push(&["refs/heads/master:refs/heads/master"], None)?;
virtual_branches::target::Writer::new(
gb_repo,
VirtualBranchesHandle::new(&project_repository.project().gb_dir()),
)?
.write_default(&virtual_branches::target::Target {
branch: "refs/remotes/origin/master".parse().unwrap(),
remote_url: remote_repo.path().to_str().unwrap().parse().unwrap(),
sha: remote_repo.head().unwrap().target().unwrap(),
})
.expect("failed to write target");
virtual_branches::integration::update_gitbutler_integration(gb_repo, project_repository)
.expect("failed to update integration");
Ok(())
}
}
pub fn init_opts() -> git2::RepositoryInitOptions {
let mut opts = git2::RepositoryInitOptions::new();
opts.initial_head("master");
opts
}
pub fn init_opts_bare() -> git2::RepositoryInitOptions {
let mut opts = init_opts();
opts.bare(true);
opts
}

View File

@ -0,0 +1,232 @@
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use tempfile::{tempdir, TempDir};
use crate::{init_opts, init_opts_bare, VAR_NO_CLEANUP};
pub struct Suite {
pub local_app_data: Option<TempDir>,
pub storage: gitbutler_core::storage::Storage,
pub users: gitbutler_core::users::Controller,
pub projects: gitbutler_core::projects::Controller,
pub keys: gitbutler_core::keys::Controller,
}
impl Drop for Suite {
fn drop(&mut self) {
if std::env::var_os(VAR_NO_CLEANUP).is_some() {
let _ = self.local_app_data.take().unwrap().into_path();
}
}
}
impl Default for Suite {
fn default() -> Self {
let local_app_data = temp_dir();
let storage = gitbutler_core::storage::Storage::new(&local_app_data);
let users = gitbutler_core::users::Controller::from_path(&local_app_data);
let projects = gitbutler_core::projects::Controller::from_path(&local_app_data);
let keys = gitbutler_core::keys::Controller::from_path(&local_app_data);
Self {
storage,
local_app_data: Some(local_app_data),
users,
projects,
keys,
}
}
}
impl Suite {
pub fn local_app_data(&self) -> &Path {
self.local_app_data.as_ref().unwrap().path()
}
pub fn sign_in(&self) -> gitbutler_core::users::User {
let user = gitbutler_core::users::User {
name: Some("test".to_string()),
email: "test@email.com".to_string(),
access_token: "token".to_string(),
..Default::default()
};
self.users.set_user(&user).expect("failed to add user");
user
}
fn project(&self, fs: HashMap<PathBuf, &str>) -> (gitbutler_core::projects::Project, TempDir) {
let (repository, tmp) = test_repository();
for (path, contents) in fs {
if let Some(parent) = path.parent() {
fs::create_dir_all(repository.path().parent().unwrap().join(parent))
.expect("failed to create dir");
}
fs::write(
repository.path().parent().unwrap().join(&path),
contents.as_bytes(),
)
.expect("failed to write file");
}
commit_all(&repository);
(
self.projects
.add(repository.path().parent().unwrap())
.expect("failed to add project"),
tmp,
)
}
pub fn new_case_with_files(&self, fs: HashMap<PathBuf, &str>) -> Case {
let (project, project_tmp) = self.project(fs);
Case::new(self, project, project_tmp)
}
pub fn new_case(&self) -> Case {
self.new_case_with_files(HashMap::new())
}
}
pub struct Case<'a> {
suite: &'a Suite,
pub project: gitbutler_core::projects::Project,
pub project_repository: gitbutler_core::project_repository::Repository,
pub gb_repository: gitbutler_core::gb_repository::Repository,
pub credentials: gitbutler_core::git::credentials::Helper,
/// The directory containing the `project_repository`
project_tmp: Option<TempDir>,
}
impl Drop for Case<'_> {
fn drop(&mut self) {
if let Some(tmp) = self
.project_tmp
.take()
.filter(|_| std::env::var_os(VAR_NO_CLEANUP).is_some())
{
let _ = tmp.into_path();
}
}
}
impl<'a> Case<'a> {
fn new(
suite: &'a Suite,
project: gitbutler_core::projects::Project,
project_tmp: TempDir,
) -> Case<'a> {
let project_repository = gitbutler_core::project_repository::Repository::open(&project)
.expect("failed to create project repository");
let gb_repository = gitbutler_core::gb_repository::Repository::open(
suite.local_app_data(),
&project_repository,
None,
)
.expect("failed to open gb repository");
let credentials =
gitbutler_core::git::credentials::Helper::from_path(suite.local_app_data());
Case {
suite,
project,
gb_repository,
project_repository,
project_tmp: Some(project_tmp),
credentials,
}
}
pub fn refresh(mut self) -> Self {
let project = self
.suite
.projects
.get(&self.project.id)
.expect("failed to get project");
let project_repository = gitbutler_core::project_repository::Repository::open(&project)
.expect("failed to create project repository");
let user = self.suite.users.get_user().expect("failed to get user");
let credentials =
gitbutler_core::git::credentials::Helper::from_path(self.suite.local_app_data());
Self {
suite: self.suite,
gb_repository: gitbutler_core::gb_repository::Repository::open(
self.suite.local_app_data(),
&project_repository,
user.as_ref(),
)
.expect("failed to open gb repository"),
credentials,
project_repository,
project,
project_tmp: self.project_tmp.take(),
}
}
}
pub fn test_database() -> (gitbutler_core::database::Database, TempDir) {
let tmp = temp_dir();
let db = gitbutler_core::database::Database::open_in_directory(&tmp).unwrap();
(db, tmp)
}
pub fn temp_dir() -> TempDir {
tempdir().unwrap()
}
pub fn empty_bare_repository() -> (gitbutler_core::git::Repository, TempDir) {
let tmp = temp_dir();
(
gitbutler_core::git::Repository::init_opts(&tmp, &init_opts_bare())
.expect("failed to init repository"),
tmp,
)
}
pub fn test_repository() -> (gitbutler_core::git::Repository, TempDir) {
let tmp = temp_dir();
let repository = gitbutler_core::git::Repository::init_opts(&tmp, &init_opts())
.expect("failed to init repository");
let mut index = repository.index().expect("failed to get index");
let oid = index.write_tree().expect("failed to write tree");
let signature = gitbutler_core::git::Signature::now("test", "test@email.com").unwrap();
repository
.commit(
Some(&"refs/heads/master".parse().unwrap()),
&signature,
&signature,
"Initial commit",
&repository.find_tree(oid).expect("failed to find tree"),
&[],
)
.expect("failed to commit");
(repository, tmp)
}
pub fn commit_all(repository: &gitbutler_core::git::Repository) -> gitbutler_core::git::Oid {
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 = gitbutler_core::git::Signature::now("test", "test@email.com").unwrap();
let head = repository.head().expect("failed to get head");
let commit_oid = repository
.commit(
Some(&head.name().unwrap()),
&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")],
)
.expect("failed to commit");
commit_oid
}

View File

@ -0,0 +1,346 @@
use std::path;
use gitbutler_core::git;
use tempfile::TempDir;
use crate::{init_opts, VAR_NO_CLEANUP};
pub fn temp_dir() -> TempDir {
tempfile::tempdir().unwrap()
}
pub struct TestProject {
local_repository: git::Repository,
local_tmp: Option<TempDir>,
remote_repository: git::Repository,
remote_tmp: Option<TempDir>,
}
impl Drop for TestProject {
fn drop(&mut self) {
if std::env::var_os(VAR_NO_CLEANUP).is_some() {
let _ = self.local_tmp.take().unwrap().into_path();
let _ = self.remote_tmp.take().unwrap().into_path();
}
}
}
impl Default for TestProject {
fn default() -> Self {
let local_tmp = temp_dir();
let local_repository = git::Repository::init_opts(local_tmp.path(), &init_opts())
.expect("failed to init repository");
let mut index = local_repository.index().expect("failed to get index");
let oid = index.write_tree().expect("failed to write tree");
let signature = git::Signature::now("test", "test@email.com").unwrap();
local_repository
.commit(
Some(&"refs/heads/master".parse().unwrap()),
&signature,
&signature,
"Initial commit",
&local_repository
.find_tree(oid)
.expect("failed to find tree"),
&[],
)
.expect("failed to commit");
let remote_tmp = temp_dir();
let remote_repository = git::Repository::init_opts(
remote_tmp.path(),
git2::RepositoryInitOptions::new()
.bare(true)
.external_template(false),
)
.expect("failed to init repository");
{
let mut remote = local_repository
.remote(
"origin",
&remote_repository
.path()
.to_str()
.expect("failed to convert path to str")
.parse()
.unwrap(),
)
.expect("failed to add remote");
remote
.push(&["refs/heads/master:refs/heads/master"], None)
.expect("failed to push");
}
Self {
local_repository,
local_tmp: Some(local_tmp),
remote_repository,
remote_tmp: Some(remote_tmp),
}
}
}
impl TestProject {
pub fn path(&self) -> &std::path::Path {
self.local_repository.workdir().unwrap()
}
pub fn push_branch(&self, branch: &git::LocalRefname) {
let mut origin = self.local_repository.find_remote("origin").unwrap();
origin.push(&[&format!("{branch}:{branch}")], None).unwrap();
}
pub fn push(&self) {
let mut origin = self.local_repository.find_remote("origin").unwrap();
origin
.push(&["refs/heads/master:refs/heads/master"], None)
.unwrap();
}
/// git add -A
/// git reset --hard <oid>
pub fn reset_hard(&self, oid: Option<git::Oid>) {
let mut index = self.local_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 head = self.local_repository.head().unwrap();
let commit = oid.map_or(head.peel_to_commit().unwrap(), |oid| {
self.local_repository.find_commit(oid).unwrap()
});
let head_ref = head.name().unwrap();
self.local_repository.find_reference(&head_ref).unwrap();
self.local_repository
.reset(&commit, git2::ResetType::Hard, None)
.unwrap();
}
/// fetch remote into local
pub fn fetch(&self) {
let mut remote = self.local_repository.find_remote("origin").unwrap();
remote
.fetch(&["+refs/heads/*:refs/remotes/origin/*"], None)
.unwrap();
}
pub fn rebase_and_merge(&self, branch_name: &git::Refname) {
let branch_name: git::Refname = match branch_name {
git::Refname::Local(local) => format!("refs/heads/{}", local.branch()).parse().unwrap(),
git::Refname::Remote(remote) => {
format!("refs/heads/{}", remote.branch()).parse().unwrap()
}
_ => "INVALID".parse().unwrap(), // todo
};
let branch = self.remote_repository.find_branch(&branch_name).unwrap();
let branch_commit = branch.peel_to_commit().unwrap();
let master_branch = {
let name: git::Refname = "refs/heads/master".parse().unwrap();
self.remote_repository.find_branch(&name).unwrap()
};
let master_branch_commit = master_branch.peel_to_commit().unwrap();
let mut rebase_options = git2::RebaseOptions::new();
rebase_options.quiet(true);
rebase_options.inmemory(true);
let mut rebase = self
.remote_repository
.rebase(
Some(branch_commit.id()),
Some(master_branch_commit.id()),
None,
Some(&mut rebase_options),
)
.unwrap();
let mut rebase_success = true;
let mut last_rebase_head = branch_commit.id();
while let Some(Ok(op)) = rebase.next() {
let commit = self.remote_repository.find_commit(op.id().into()).unwrap();
let index = rebase.inmemory_index().unwrap();
if index.has_conflicts() {
rebase_success = false;
break;
}
if let Ok(commit_id) = rebase.commit(None, &commit.committer().into(), None) {
last_rebase_head = commit_id.into();
} else {
rebase_success = false;
break;
};
}
if rebase_success {
self.remote_repository
.reference(
&"refs/heads/master".parse().unwrap(),
last_rebase_head,
true,
&format!("rebase: {}", branch_name),
)
.unwrap();
} else {
rebase.abort().unwrap();
}
}
/// works like if we'd open and merge a PR on github. does not update local.
pub fn merge(&self, branch_name: &git::Refname) {
let branch_name: git::Refname = match branch_name {
git::Refname::Local(local) => format!("refs/heads/{}", local.branch()).parse().unwrap(),
git::Refname::Remote(remote) => {
format!("refs/heads/{}", remote.branch()).parse().unwrap()
}
_ => "INVALID".parse().unwrap(), // todo
};
let branch = self.remote_repository.find_branch(&branch_name).unwrap();
let branch_commit = branch.peel_to_commit().unwrap();
let master_branch = {
let name: git::Refname = "refs/heads/master".parse().unwrap();
self.remote_repository.find_branch(&name).unwrap()
};
let master_branch_commit = master_branch.peel_to_commit().unwrap();
let merge_base = {
let oid = self
.remote_repository
.merge_base(branch_commit.id(), master_branch_commit.id())
.unwrap();
self.remote_repository.find_commit(oid).unwrap()
};
let merge_tree = {
let mut merge_index = self
.remote_repository
.merge_trees(
&merge_base.tree().unwrap(),
&master_branch.peel_to_tree().unwrap(),
&branch.peel_to_tree().unwrap(),
)
.unwrap();
let oid = merge_index.write_tree_to(&self.remote_repository).unwrap();
self.remote_repository.find_tree(oid).unwrap()
};
self.remote_repository
.commit(
Some(&"refs/heads/master".parse().unwrap()),
&branch_commit.author(),
&branch_commit.committer(),
&format!("Merge pull request from {}", branch_name),
&merge_tree,
&[&master_branch_commit, &branch_commit],
)
.unwrap();
}
pub fn find_commit(&self, oid: git::Oid) -> Result<git::Commit, git::Error> {
self.local_repository.find_commit(oid)
}
pub fn checkout_commit(&self, commit_oid: git::Oid) {
let commit = self.local_repository.find_commit(commit_oid).unwrap();
let commit_tree = commit.tree().unwrap();
self.local_repository.set_head_detached(commit_oid).unwrap();
self.local_repository
.checkout_tree(&commit_tree)
.force()
.checkout()
.unwrap();
}
pub fn checkout(&self, branch: &git::LocalRefname) {
let branch: git::Refname = branch.into();
let tree = match self.local_repository.find_branch(&branch) {
Ok(branch) => branch.peel_to_tree(),
Err(git::Error::NotFound(_)) => {
let head_commit = self
.local_repository
.head()
.unwrap()
.peel_to_commit()
.unwrap();
self.local_repository
.reference(&branch, head_commit.id(), false, "new branch")
.unwrap();
head_commit.tree()
}
Err(error) => Err(error),
}
.unwrap();
self.local_repository.set_head(&branch).unwrap();
self.local_repository
.checkout_tree(&tree)
.force()
.checkout()
.unwrap();
}
/// takes all changes in the working directory and commits them into local
pub fn commit_all(&self, message: &str) -> git::Oid {
let head = self.local_repository.head().unwrap();
let mut index = self.local_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 = git::Signature::now("test", "test@email.com").unwrap();
self.local_repository
.commit(
head.name().as_ref(),
&signature,
&signature,
message,
&self
.local_repository
.find_tree(oid)
.expect("failed to find tree"),
&[&self
.local_repository
.find_commit(
self.local_repository
.refname_to_id("HEAD")
.expect("failed to get head"),
)
.expect("failed to find commit")],
)
.expect("failed to commit")
}
pub fn references(&self) -> Vec<git::Reference> {
self.local_repository
.references()
.expect("failed to get references")
.collect::<Result<Vec<_>, _>>()
.expect("failed to read references")
}
pub fn add_submodule(&self, url: &git::Url, path: &path::Path) {
let mut submodule = self.local_repository.add_submodule(url, path).unwrap();
let repo = submodule.open().unwrap();
// checkout submodule's master head
repo.find_remote("origin")
.unwrap()
.fetch(&["+refs/heads/*:refs/heads/*"], None, None)
.unwrap();
let reference = repo.find_reference("refs/heads/master").unwrap();
let reference_head = repo.find_commit(reference.target().unwrap()).unwrap();
repo.checkout_tree(reference_head.tree().unwrap().as_object(), None)
.unwrap();
// be sure that `HEAD` points to the actual head - `git2` seems to initialize it
// with `init.defaultBranch`, causing failure otherwise.
repo.set_head("refs/heads/master").unwrap();
submodule.add_finalize().unwrap();
}
}