use git2::Repository;
use std::sync::Arc;
use tempfile::TempDir;
use trustfall_git_adapter::GitAdapter;
fn create_test_repo_with_tags() -> (TempDir, Repository) {
let (temp_dir, repo) = create_test_repo_with_multiple_commits();
let signature = git2::Signature::now("Test User", "test@example.com").unwrap();
{
let head_commit = repo.head().unwrap().peel_to_commit().unwrap();
repo.tag_lightweight("v0.1.0", head_commit.as_object(), false)
.unwrap();
repo.tag(
"v1.0.0",
head_commit.as_object(),
&signature,
"Release version 1.0.0",
false,
)
.unwrap();
let first_commit_oid = head_commit.parent_id(0).unwrap();
let first_commit = repo.find_commit(first_commit_oid).unwrap();
repo.tag(
"v0.0.1",
first_commit.as_object(),
&signature,
"Initial release",
false,
)
.unwrap();
}
(temp_dir, repo)
}
fn create_test_repo() -> (TempDir, Repository) {
let temp_dir = TempDir::new().unwrap();
let repo = Repository::init(temp_dir.path()).unwrap();
{
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
}
let signature = git2::Signature::now("Test User", "test@example.com").unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
{
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(
Some("HEAD"),
&signature,
&signature,
"Initial commit",
&tree,
&[],
)
.unwrap();
}
(temp_dir, repo)
}
fn create_test_repo_with_multiple_commits() -> (TempDir, Repository) {
let temp_dir = TempDir::new().unwrap();
let repo = Repository::init(temp_dir.path()).unwrap();
{
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
}
let signature = git2::Signature::now("Test User", "test@example.com").unwrap();
let author_signature = git2::Signature::now("Author User", "author@example.com").unwrap();
let first_commit = {
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(
Some("HEAD"),
&author_signature,
&signature,
"Initial commit",
&tree,
&[],
)
.unwrap()
};
{
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
let first_commit_obj = repo.find_commit(first_commit).unwrap();
repo.commit(
Some("HEAD"),
&author_signature,
&signature,
"Second commit with more details",
&tree,
&[&first_commit_obj],
)
.unwrap();
}
(temp_dir, repo)
}
#[test]
fn test_adapter_creation() {
let (_temp_dir, repo) = create_test_repo();
let adapter = GitAdapter::new(&repo);
let _schema = adapter.schema();
}
#[test]
fn test_query_repository_name() {
let (_temp_dir, repo) = create_test_repo();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
name @output
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(results.len(), 1);
assert!(results[0].contains_key("name"));
}
#[test]
fn test_query_repository_branches() {
let (_temp_dir, repo) = create_test_repo();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
branches {
name @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(!results.is_empty());
let branch_names: Vec<_> = results
.iter()
.filter_map(|row| {
if let Some(trustfall::FieldValue::String(name)) = row.get("name") {
Some(name.as_ref())
} else {
None
}
})
.collect();
assert!(branch_names.contains(&"main") || branch_names.contains(&"master"));
}
#[test]
fn test_query_repository_commits() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
hash @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(results.len() >= 2);
for result in &results {
assert!(result.contains_key("hash"));
if let Some(trustfall::FieldValue::String(hash)) = result.get("hash") {
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
} else {
panic!("Hash field should be a string");
}
}
}
#[test]
fn test_query_commit_properties() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
hash @output
message @output
author @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(!results.is_empty());
for result in &results {
assert!(result.contains_key("hash"));
if let Some(trustfall::FieldValue::String(hash)) = result.get("hash") {
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
} else {
panic!("Hash field should be a string");
}
assert!(result.contains_key("message"));
if let Some(trustfall::FieldValue::String(message)) = result.get("message") {
assert!(!message.is_empty());
} else {
panic!("Message field should be a string");
}
assert!(result.contains_key("author"));
if let Some(trustfall::FieldValue::String(author)) = result.get("author") {
assert!(!author.is_empty());
} else {
panic!("Author field should be a string");
}
}
}
#[test]
fn test_query_specific_commit_content() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
message @output
author @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(results.len() >= 2);
let messages: Vec<String> = results
.iter()
.filter_map(|row| {
if let Some(trustfall::FieldValue::String(msg)) = row.get("message") {
Some(msg.to_string())
} else {
None
}
})
.collect();
assert!(messages.contains(&"Initial commit".to_string()));
assert!(messages.contains(&"Second commit with more details".to_string()));
let authors: Vec<String> = results
.iter()
.filter_map(|row| {
if let Some(trustfall::FieldValue::String(author)) = row.get("author") {
Some(author.to_string())
} else {
None
}
})
.collect();
assert!(authors.contains(&"Author User".to_string()));
}
#[test]
fn test_query_branch_commit_relationship() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
branches {
name @output
commit {
hash @output
message @output
}
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(!results.is_empty());
for result in &results {
assert!(result.contains_key("name"));
assert!(result.contains_key("hash"));
assert!(result.contains_key("message"));
if let Some(trustfall::FieldValue::String(hash)) = result.get("hash") {
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
if let Some(trustfall::FieldValue::String(message)) = result.get("message") {
assert_eq!(message.as_ref(), "Second commit with more details");
}
}
}
#[test]
fn test_query_commits_with_limit() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits(limit: 1) {
hash @output
message @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(results.len(), 1);
let result = &results[0];
assert!(result.contains_key("hash"));
assert!(result.contains_key("message"));
if let Some(trustfall::FieldValue::String(hash)) = result.get("hash") {
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
} else {
panic!("Hash field should be a string");
}
if let Some(trustfall::FieldValue::String(message)) = result.get("message") {
assert_eq!(message.as_ref(), "Second commit with more details");
} else {
panic!("Message field should be a string");
}
}
#[test]
fn test_query_commit_date() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
hash @output
date @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(results.len() >= 2);
for result in &results {
assert!(result.contains_key("date"));
if let Some(trustfall::FieldValue::String(date)) = result.get("date") {
assert!(!date.is_empty());
assert!(date.contains('T'));
assert!(date.len() >= 19);
} else {
panic!("Date field should be a string");
}
}
}
#[test]
fn test_query_commit_author_and_committer_fields() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
author @output
author_email @output
committer @output
committer_email @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(results.len() >= 2);
for result in &results {
if let Some(trustfall::FieldValue::String(author)) = result.get("author") {
assert_eq!(author.as_ref(), "Author User");
} else {
panic!("author field should be a string");
}
if let Some(trustfall::FieldValue::String(email)) = result.get("author_email") {
assert_eq!(email.as_ref(), "author@example.com");
} else {
panic!("author_email field should be a string");
}
if let Some(trustfall::FieldValue::String(committer)) = result.get("committer") {
assert_eq!(committer.as_ref(), "Test User");
} else {
panic!("committer field should be a string");
}
if let Some(trustfall::FieldValue::String(email)) = result.get("committer_email") {
assert_eq!(email.as_ref(), "test@example.com");
} else {
panic!("committer_email field should be a string");
}
}
}
#[test]
fn test_query_commits_with_different_limits() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query_no_limit = r#"
{
repository {
commits {
hash @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results_no_limit: Vec<_> = trustfall::execute_query(
adapter.schema(),
Arc::new(&adapter),
query_no_limit,
variables.clone(),
)
.unwrap()
.collect();
let query_limit_1 = r#"
{
repository {
commits(limit: 1) {
hash @output
}
}
}
"#;
let results_limit_1: Vec<_> = trustfall::execute_query(
adapter.schema(),
Arc::new(&adapter),
query_limit_1,
variables.clone(),
)
.unwrap()
.collect();
let query_limit_5 = r#"
{
repository {
commits(limit: 5) {
hash @output
}
}
}
"#;
let results_limit_5: Vec<_> = trustfall::execute_query(
adapter.schema(),
Arc::new(&adapter),
query_limit_5,
variables,
)
.unwrap()
.collect();
assert!(
results_no_limit.len() >= 2,
"Should have at least 2 commits without limit"
);
assert_eq!(
results_limit_1.len(),
1,
"Should have exactly 1 commit with limit 1"
);
assert_eq!(
results_limit_5.len(),
results_no_limit.len(),
"Limit 5 should return all available commits"
);
if let (Some(first_no_limit), Some(first_limit_1)) =
(results_no_limit.first(), results_limit_1.first())
{
assert_eq!(
first_no_limit.get("hash"),
first_limit_1.get("hash"),
"Limited query should return the same first commit"
);
}
}
#[test]
fn test_query_tag_names() {
let (_temp_dir, repo) = create_test_repo_with_tags();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
tags {
name @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(results.len(), 3, "Should have 3 tags");
let tag_names: Vec<String> = results
.iter()
.filter_map(|row| {
if let Some(trustfall::FieldValue::String(name)) = row.get("name") {
Some(name.to_string())
} else {
None
}
})
.collect();
assert!(tag_names.contains(&"v0.1.0".to_string()));
assert!(tag_names.contains(&"v1.0.0".to_string()));
assert!(tag_names.contains(&"v0.0.1".to_string()));
}
#[test]
fn test_query_annotated_tag_properties() {
let (_temp_dir, repo) = create_test_repo_with_tags();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
tags {
name @output
message @output
tagger_name @output
tagger_email @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
let v1_tag = results
.iter()
.find(|row| {
matches!(row.get("name"), Some(trustfall::FieldValue::String(name)) if name.as_ref() == "v1.0.0")
})
.expect("Should find v1.0.0 tag");
assert_eq!(
v1_tag.get("message"),
Some(&trustfall::FieldValue::String(
"Release version 1.0.0".into()
))
);
assert_eq!(
v1_tag.get("tagger_name"),
Some(&trustfall::FieldValue::String("Test User".into()))
);
assert_eq!(
v1_tag.get("tagger_email"),
Some(&trustfall::FieldValue::String("test@example.com".into()))
);
let lightweight_tag = results
.iter()
.find(|row| {
matches!(row.get("name"), Some(trustfall::FieldValue::String(name)) if name.as_ref() == "v0.1.0")
})
.expect("Should find v0.1.0 tag");
assert_eq!(
lightweight_tag.get("message"),
Some(&trustfall::FieldValue::Null)
);
assert_eq!(
lightweight_tag.get("tagger_name"),
Some(&trustfall::FieldValue::Null)
);
assert_eq!(
lightweight_tag.get("tagger_email"),
Some(&trustfall::FieldValue::Null)
);
}
#[test]
fn test_query_tag_commit_relationship() {
let (_temp_dir, repo) = create_test_repo_with_tags();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
tags {
name @output
commit {
hash @output
message @output
}
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(results.len(), 3);
for result in &results {
assert!(result.contains_key("name"));
assert!(result.contains_key("hash"));
assert!(result.contains_key("message"));
if let Some(trustfall::FieldValue::String(hash)) = result.get("hash") {
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
} else {
panic!("Hash field should be a string");
}
}
let v001 = results
.iter()
.find(|row| {
matches!(row.get("name"), Some(trustfall::FieldValue::String(name)) if name.as_ref() == "v0.0.1")
})
.expect("Should find v0.0.1 tag");
if let Some(trustfall::FieldValue::String(msg)) = v001.get("message") {
assert_eq!(msg.as_ref(), "Initial commit");
}
let v100 = results
.iter()
.find(|row| {
matches!(row.get("name"), Some(trustfall::FieldValue::String(name)) if name.as_ref() == "v1.0.0")
})
.expect("Should find v1.0.0 tag");
if let Some(trustfall::FieldValue::String(msg)) = v100.get("message") {
assert_eq!(msg.as_ref(), "Second commit with more details");
}
}
#[test]
fn test_query_tags_empty_repo() {
let (_temp_dir, repo) = create_test_repo();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
tags {
name @output
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(
results.is_empty(),
"Repo with no tags should return empty results"
);
}
#[test]
fn test_query_lightweight_tag_commit() {
let (_temp_dir, repo) = create_test_repo_with_tags();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
tags {
name @output
commit {
hash @output
}
}
}
}
"#;
let variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
let lightweight = results
.iter()
.find(|row| {
matches!(row.get("name"), Some(trustfall::FieldValue::String(name)) if name.as_ref() == "v0.1.0")
})
.expect("Should find v0.1.0 lightweight tag");
let head_oid = repo.head().unwrap().peel_to_commit().unwrap().id();
let expected_hash = head_oid.to_string();
if let Some(trustfall::FieldValue::String(hash)) = lightweight.get("hash") {
assert_eq!(
hash.as_ref(),
expected_hash,
"Lightweight tag should point to HEAD commit"
);
} else {
panic!("Hash field should be a string");
}
}
#[test]
fn test_filter_commits_by_author() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
author @output @filter(op: "=", value: ["$author"])
message @output
}
}
}
"#;
let mut variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
variables.insert("author", "Author User");
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert!(!results.is_empty(), "Should find commits by Author User");
for result in &results {
if let Some(trustfall::FieldValue::String(author)) = result.get("author") {
assert_eq!(author.as_ref(), "Author User");
} else {
panic!("Author field should be a string");
}
}
}
#[test]
fn test_filter_commits_by_message_regex() {
let (_temp_dir, repo) = create_test_repo_with_multiple_commits();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
commits {
message @output @filter(op: "regex", value: ["$pattern"])
}
}
}
"#;
let mut variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
variables.insert("pattern", "^Second");
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(
results.len(),
1,
"Should find exactly 1 commit starting with 'Second'"
);
if let Some(trustfall::FieldValue::String(message)) = results[0].get("message") {
assert!(
message.starts_with("Second"),
"Message should start with 'Second'"
);
} else {
panic!("Message field should be a string");
}
}
#[test]
fn test_filter_branches_by_name() {
let (_temp_dir, repo) = create_test_repo();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
branches {
name @output @filter(op: "regex", value: ["$pattern"])
}
}
}
"#;
let mut variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
variables.insert("pattern", "ma(in|ster)");
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(
results.len(),
1,
"Should find exactly 1 branch matching main or master"
);
}
#[test]
fn test_filter_tags_by_name() {
let (_temp_dir, repo) = create_test_repo_with_tags();
let adapter = GitAdapter::new(&repo);
let query = r#"
{
repository {
tags {
name @output @filter(op: "=", value: ["$name"])
message @output
}
}
}
"#;
let mut variables: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
variables.insert("name", "v1.0.0");
let results: Vec<_> =
trustfall::execute_query(adapter.schema(), Arc::new(&adapter), query, variables)
.unwrap()
.collect();
assert_eq!(results.len(), 1, "Should find exactly 1 tag named v1.0.0");
let result = &results[0];
if let Some(trustfall::FieldValue::String(name)) = result.get("name") {
assert_eq!(name.as_ref(), "v1.0.0");
} else {
panic!("Name field should be a string");
}
match result.get("message") {
Some(trustfall::FieldValue::String(msg)) => {
assert!(
!msg.is_empty(),
"Annotated tag should have a non-empty message"
);
}
_ => panic!("Message field should be a non-null string for annotated tag"),
}
}