#![cfg(test)]
#![allow(clippy::unwrap_used)]
use std::path::Path;
use panproto_vcs::{MemStore, Store};
use crate::export::export_to_git;
use crate::import::{import_git_repo, import_git_repo_incremental};
fn create_test_git_repo(files: &[(&str, &[u8])]) -> (tempfile::TempDir, git2::Repository) {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
let sig = git2::Signature::new("Test", "test@example.com", &git2::Time::new(1000, 0)).unwrap();
let mut index = repo.index().unwrap();
for (path, content) in files {
let full_path = dir.path().join(path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&full_path, content).unwrap();
index.add_path(Path::new(path)).unwrap();
}
index.write().unwrap();
let tree_oid = index.write_tree().unwrap();
{
let tree = repo.find_tree(tree_oid).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
.unwrap();
}
(dir, repo)
}
#[test]
fn import_single_typescript_file() {
let (_dir, git_repo) = create_test_git_repo(&[(
"main.ts",
b"function greet(name: string): string { return 'Hello, ' + name; }",
)]);
let mut store = MemStore::new();
let result = import_git_repo(&git_repo, &mut store, "HEAD").unwrap();
assert_eq!(result.commit_count, 1);
assert_ne!(result.head_id, panproto_vcs::ObjectId::ZERO);
assert_eq!(result.oid_map.len(), 1);
let commit_obj = store.get(&result.head_id).unwrap();
match &commit_obj {
panproto_vcs::Object::Commit(c) => {
assert_eq!(c.message, "Initial commit");
assert_eq!(c.author, "Test");
}
other => panic!("expected commit, got {}", other.type_name()),
}
}
#[test]
fn import_multi_file_project() {
let (_dir, git_repo) = create_test_git_repo(&[
(
"src/main.ts",
b"function main(): void { console.log('hello'); }",
),
(
"src/utils.ts",
b"export function add(a: number, b: number): number { return a + b; }",
),
("README.md", b"# Test Project\n\nA test project.\n"),
]);
let mut store = MemStore::new();
let result = import_git_repo(&git_repo, &mut store, "HEAD").unwrap();
assert_eq!(result.commit_count, 1);
let commit_obj = store.get(&result.head_id).unwrap();
let commit = match &commit_obj {
panproto_vcs::Object::Commit(c) => c,
other => panic!("expected commit, got {}", other.type_name()),
};
let schema_obj = store.get(&commit.schema_id).unwrap();
match &schema_obj {
panproto_vcs::Object::Schema(s) => {
assert!(
s.vertices.len() > 5,
"expected rich project schema, got {} vertices",
s.vertices.len()
);
}
other => panic!("expected schema, got {}", other.type_name()),
}
}
#[test]
fn import_multiple_commits() {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
let sig = git2::Signature::new("Dev", "dev@test.com", &git2::Time::new(1000, 0)).unwrap();
let file_path = dir.path().join("main.py");
std::fs::write(&file_path, b"x = 1\n").unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new("main.py")).unwrap();
index.write().unwrap();
let tree_oid = index.write_tree().unwrap();
let tree = repo.find_tree(tree_oid).unwrap();
let commit1_oid = repo
.commit(Some("HEAD"), &sig, &sig, "First", &tree, &[])
.unwrap();
std::fs::write(&file_path, b"x = 1\ny = 2\n").unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new("main.py")).unwrap();
index.write().unwrap();
let tree_oid = index.write_tree().unwrap();
let tree = repo.find_tree(tree_oid).unwrap();
let commit1 = repo.find_commit(commit1_oid).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Second", &tree, &[&commit1])
.unwrap();
let mut store = MemStore::new();
let result = import_git_repo(&repo, &mut store, "HEAD").unwrap();
assert_eq!(result.commit_count, 2);
assert_eq!(result.oid_map.len(), 2);
let second_commit_obj = store.get(&result.head_id).unwrap();
match &second_commit_obj {
panproto_vcs::Object::Commit(c) => {
assert_eq!(c.message, "Second");
assert_eq!(c.parents.len(), 1);
let first_panproto_id = result.oid_map[0].1;
assert_eq!(c.parents[0], first_panproto_id);
}
other => panic!("expected commit, got {}", other.type_name()),
}
}
fn create_linear_history(n: usize) -> (tempfile::TempDir, git2::Repository, Vec<git2::Oid>) {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
let sig = git2::Signature::new("Dev", "dev@test.com", &git2::Time::new(1000, 0)).unwrap();
let file_path = dir.path().join("main.py");
let mut commit_oids = Vec::new();
let mut parent: Option<git2::Oid> = None;
for i in 0..n {
std::fs::write(&file_path, format!("x = {i}\n").as_bytes()).unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new("main.py")).unwrap();
index.write().unwrap();
let tree_oid = index.write_tree().unwrap();
let tree = repo.find_tree(tree_oid).unwrap();
let parent_commit = parent.map(|p| repo.find_commit(p).unwrap());
let parents: Vec<&git2::Commit<'_>> = parent_commit.iter().collect();
let new_oid = repo
.commit(
Some("HEAD"),
&sig,
&sig,
&format!("commit {i}"),
&tree,
&parents,
)
.unwrap();
commit_oids.push(new_oid);
parent = Some(new_oid);
}
(dir, repo, commit_oids)
}
#[test]
fn incremental_import_skips_known_ancestors() {
let (_dir, repo, commit_oids) = create_linear_history(3);
let mut store = MemStore::new();
let first = import_git_repo(&repo, &mut store, &commit_oids[1].to_string()).unwrap();
assert_eq!(first.commit_count, 2);
let known: rustc_hash::FxHashMap<git2::Oid, panproto_vcs::ObjectId> =
first.oid_map.iter().copied().collect();
let second =
import_git_repo_incremental(&repo, &mut store, &commit_oids[2].to_string(), &known)
.unwrap();
assert_eq!(second.commit_count, 1, "expected only one new commit");
assert_eq!(second.oid_map.len(), 1);
assert_eq!(second.oid_map[0].0, commit_oids[2]);
let head_obj = store.get(&second.head_id).unwrap();
match &head_obj {
panproto_vcs::Object::Commit(c) => {
assert_eq!(c.parents.len(), 1);
assert_eq!(c.parents[0], known[&commit_oids[1]]);
}
other => panic!("expected commit, got {}", other.type_name()),
}
}
#[test]
fn incremental_import_noop_when_head_is_known() {
let (_dir, repo, commit_oids) = create_linear_history(2);
let mut store = MemStore::new();
let first = import_git_repo(&repo, &mut store, "HEAD").unwrap();
assert_eq!(first.commit_count, 2);
let known: rustc_hash::FxHashMap<git2::Oid, panproto_vcs::ObjectId> =
first.oid_map.iter().copied().collect();
let second = import_git_repo_incremental(&repo, &mut store, "HEAD", &known).unwrap();
assert_eq!(second.commit_count, 0);
assert_eq!(second.head_id, known[&commit_oids[1]]);
assert!(second.oid_map.is_empty());
}
#[test]
fn incremental_import_sets_no_local_refs() {
let (_dir, repo, _oids) = create_linear_history(2);
let mut store = MemStore::new();
let result = import_git_repo(&repo, &mut store, "HEAD").unwrap();
assert!(result.head_id != panproto_vcs::ObjectId::ZERO);
let refs = store.list_refs("refs/").unwrap();
assert!(
refs.is_empty(),
"expected no refs after import, found: {refs:?}"
);
}
#[test]
fn incremental_import_tolerates_stale_known_entries() {
let (_dir, repo, commit_oids) = create_linear_history(2);
let fake_oid = git2::Oid::from_str("0123456789abcdef0123456789abcdef01234567").unwrap();
let mut first_store = MemStore::new();
let first = import_git_repo(&repo, &mut first_store, &commit_oids[0].to_string()).unwrap();
let first_panproto = first.oid_map[0].1;
let mut known: rustc_hash::FxHashMap<git2::Oid, panproto_vcs::ObjectId> =
rustc_hash::FxHashMap::default();
known.insert(fake_oid, panproto_vcs::ObjectId::ZERO);
known.insert(commit_oids[0], first_panproto);
let mut store = first_store;
let result = import_git_repo_incremental(&repo, &mut store, "HEAD", &known).unwrap();
assert_eq!(result.commit_count, 1);
let head_obj = store.get(&result.head_id).unwrap();
match &head_obj {
panproto_vcs::Object::Commit(c) => {
assert_eq!(c.parents, vec![first_panproto]);
}
other => panic!("expected commit, got {}", other.type_name()),
}
}
fn empty_git_repo() -> (tempfile::TempDir, git2::Repository) {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
(dir, repo)
}
#[test]
fn export_with_update_ref_none_leaves_head_unborn() {
let (_src_dir, src_repo, _oids) = create_linear_history(1);
let mut store = MemStore::new();
let import_result = import_git_repo(&src_repo, &mut store, "HEAD").unwrap();
let (_dst_dir, dst_repo) = empty_git_repo();
let parent_map: rustc_hash::FxHashMap<panproto_vcs::ObjectId, git2::Oid> =
rustc_hash::FxHashMap::default();
let result =
export_to_git(&store, &dst_repo, import_result.head_id, &parent_map, None).unwrap();
assert!(dst_repo.find_commit(result.git_oid).is_ok());
assert!(
dst_repo.head().is_err(),
"HEAD should remain unborn when update_ref is None"
);
}
#[test]
fn export_with_update_ref_some_moves_named_ref() {
let (_src_dir, src_repo, _oids) = create_linear_history(1);
let mut store = MemStore::new();
let import_result = import_git_repo(&src_repo, &mut store, "HEAD").unwrap();
let (_dst_dir, dst_repo) = empty_git_repo();
let parent_map: rustc_hash::FxHashMap<panproto_vcs::ObjectId, git2::Oid> =
rustc_hash::FxHashMap::default();
let result = export_to_git(
&store,
&dst_repo,
import_result.head_id,
&parent_map,
Some("HEAD"),
)
.unwrap();
let head = dst_repo.head().unwrap();
let head_commit = head.peel_to_commit().unwrap();
assert_eq!(head_commit.id(), result.git_oid);
}
#[test]
fn export_parent_map_links_exported_parent() {
let (_src_dir, src_repo, _oids) = create_linear_history(2);
let mut store = MemStore::new();
let import_result = import_git_repo(&src_repo, &mut store, "HEAD").unwrap();
let first_panproto = import_result.oid_map[0].1;
let second_panproto = import_result.oid_map[1].1;
let (_dst_dir, dst_repo) = empty_git_repo();
let mut parent_map: rustc_hash::FxHashMap<panproto_vcs::ObjectId, git2::Oid> =
rustc_hash::FxHashMap::default();
let first_result = export_to_git(&store, &dst_repo, first_panproto, &parent_map, None).unwrap();
parent_map.insert(first_panproto, first_result.git_oid);
let second_result =
export_to_git(&store, &dst_repo, second_panproto, &parent_map, None).unwrap();
let second_git = dst_repo.find_commit(second_result.git_oid).unwrap();
assert_eq!(second_git.parent_count(), 1);
assert_eq!(second_git.parent(0).unwrap().id(), first_result.git_oid);
}
#[test]
fn export_parent_map_empty_produces_root_commit() {
let (_src_dir, src_repo, _oids) = create_linear_history(2);
let mut store = MemStore::new();
let import_result = import_git_repo(&src_repo, &mut store, "HEAD").unwrap();
let second_panproto = import_result.oid_map[1].1;
let (_dst_dir, dst_repo) = empty_git_repo();
let parent_map: rustc_hash::FxHashMap<panproto_vcs::ObjectId, git2::Oid> =
rustc_hash::FxHashMap::default();
let result = export_to_git(&store, &dst_repo, second_panproto, &parent_map, None).unwrap();
let git_commit = dst_repo.find_commit(result.git_oid).unwrap();
assert_eq!(
git_commit.parent_count(),
0,
"unmapped panproto parents should not produce git parents"
);
}