mod common; use gitks::pb::commit_service_server::CommitService; use gitks::pb::tree_service_server::TreeService; use gitks::pb::*; fn hdr() -> RepositoryHeader { RepositoryHeader { relative_path: "test-repo".into(), ..Default::default() } } #[tokio::test] async fn test_get_commit_with_author() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let commit = svc .get_commit(tonic::Request::new(GetCommitRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), include_stats: false, include_raw: false, })) .await .unwrap() .into_inner(); assert!(commit.author.is_some(), "author must be populated"); let author = commit.author.as_ref().unwrap(); assert!( author.identity.is_some(), "author identity must be populated" ); let id = author.identity.as_ref().unwrap(); assert_eq!(id.name, "Test", "author name should be 'Test'"); assert_eq!(id.email, "test@example.com"); assert!(commit.committer.is_some(), "committer must be populated"); assert!( commit.authored_at.is_some(), "authored_at must be populated" ); assert!( commit.committed_at.is_some(), "committed_at must be populated" ); assert!( commit.authored_at.as_ref().unwrap().seconds > 0, "timestamp must be non-zero" ); } #[tokio::test] async fn test_get_commit_subject_body() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let commit = svc .get_commit(tonic::Request::new(GetCommitRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main~2".into(), })), }), include_stats: false, include_raw: false, })) .await .unwrap() .into_inner(); assert_eq!(commit.subject, "second commit"); assert!(!commit.message.is_empty()); assert!(commit.oid.is_some()); assert!(!commit.parent_oids.is_empty()); } #[tokio::test] async fn test_get_commit_with_raw() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let commit = svc .get_commit(tonic::Request::new(GetCommitRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), include_stats: false, include_raw: true, })) .await .unwrap() .into_inner(); assert!( !commit.raw.is_empty(), "raw data must be present when requested" ); let raw_str = String::from_utf8_lossy(&commit.raw); assert!(raw_str.contains("tree"), "raw should contain tree line"); assert!(raw_str.contains("author"), "raw should contain author line"); } #[tokio::test] async fn test_list_commits_with_pagination() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let page1 = svc .list_commits(tonic::Request::new(ListCommitsRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), path: String::new(), since: None, until: None, first_parent: false, all: false, reverse: false, max_parents: 0, min_parents: 0, pagination: Some(Pagination { page_size: 2, page_token: String::new(), }), })) .await .unwrap() .into_inner(); assert_eq!(page1.commits.len(), 2); let pi = page1.page_info.unwrap(); assert!(pi.has_next_page); let page2 = svc .list_commits(tonic::Request::new(ListCommitsRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), path: String::new(), since: None, until: None, first_parent: false, all: false, reverse: false, max_parents: 0, min_parents: 0, pagination: Some(Pagination { page_size: 2, page_token: pi.next_page_token, }), })) .await .unwrap() .into_inner(); assert!(!page2.commits.is_empty()); assert_ne!(page1.commits[0].oid, page2.commits[0].oid); } #[tokio::test] async fn test_get_commit_ancestors_pagination() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let page1 = svc .get_commit_ancestors(tonic::Request::new(GetCommitAncestorsRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), first_parent: false, pagination: Some(Pagination { page_size: 2, page_token: String::new(), }), })) .await .unwrap() .into_inner(); assert_eq!(page1.commits.len(), 2); let pi = page1.page_info.unwrap(); assert!(pi.has_next_page, "should have next page"); assert!( !pi.next_page_token.is_empty(), "next_page_token must be set" ); let page2 = svc .get_commit_ancestors(tonic::Request::new(GetCommitAncestorsRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), first_parent: false, pagination: Some(Pagination { page_size: 2, page_token: pi.next_page_token, }), })) .await .unwrap() .into_inner(); assert!(!page2.commits.is_empty(), "page 2 should have commits"); assert_ne!( page1.commits[0].oid.as_ref().unwrap().hex, page2.commits[0].oid.as_ref().unwrap().hex, ); } #[tokio::test] async fn test_compare_commits() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let result = svc .compare_commits(tonic::Request::new(CompareCommitsRequest { repository: Some(hdr()), base: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "feature".into(), })), }), head: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), straight: false, first_parent: false, pagination: Some(Pagination { page_size: 100, page_token: String::new(), }), })) .await .unwrap() .into_inner(); assert!(!result.commits.is_empty()); assert!(result.merge_base.is_some()); let stats = result.stats.unwrap(); assert!(stats.additions > 0); } #[tokio::test] async fn test_create_commit_and_cherry_pick() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let created = svc .create_commit(tonic::Request::new(CreateCommitRequest { repository: Some(hdr()), branch: "feature".into(), message: "cherry-pick source".into(), author: Some(Signature { identity: Some(Identity { name: "Author".into(), email: "author@test.com".into(), }), ..Default::default() }), committer: Some(Signature { identity: Some(Identity { name: "Committer".into(), email: "committer@test.com".into(), }), ..Default::default() }), actions: vec![CreateCommitAction { action: create_commit_action::Action::CreateCommitActionCreate as i32, file_path: "cp_file.txt".into(), previous_path: String::new(), content: b"cherry pick me".to_vec(), encoding: String::new(), executable: false, last_commit_oid: None, }], start_revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "feature".into(), })), }), force: false, trailers: vec![], })) .await .unwrap() .into_inner(); let source_oid = created .commit .as_ref() .unwrap() .oid .as_ref() .unwrap() .hex .clone(); let cp_result = svc .cherry_pick_commit(tonic::Request::new(CherryPickCommitRequest { repository: Some(hdr()), commit: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: source_oid.clone(), })), }), branch: "main".into(), committer: Some(Signature { identity: Some(Identity { name: "CP Committer".into(), email: "cp@test.com".into(), }), ..Default::default() }), message: String::new(), mainline: 0, })) .await .unwrap() .into_inner(); let cp_commit = cp_result.commit.unwrap(); assert_eq!(cp_commit.subject, "cherry-pick source"); let blob = svc .get_blob(tonic::Request::new(GetBlobRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), path: "cp_file.txt".into(), oid: None, max_bytes: 0, })) .await .unwrap() .into_inner(); assert_eq!(blob.data, b"cherry pick me"); } #[tokio::test] async fn test_cherry_pick_root_commit() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let work_dir = dir.path().join("work"); common::run(&work_dir, &["checkout", "--orphan", "root-source"]); common::run(&work_dir, &["rm", "-rf", "."]); std::fs::write(work_dir.join("root_only.txt"), "from root\n").unwrap(); common::run(&work_dir, &["add", "."]); common::run(&work_dir, &["commit", "-m", "standalone root"]); common::run(&work_dir, &["push", "-f", "origin", "root-source"]); let root_oid = common::run_git(&work_dir, &["rev-parse", "root-source"]) .stdout_capture() .stderr_capture() .run() .expect("find root commit") .stdout; let root_oid = String::from_utf8(root_oid).unwrap().trim().to_string(); svc.cherry_pick_commit(tonic::Request::new(CherryPickCommitRequest { repository: Some(hdr()), commit: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: root_oid, })), }), branch: "feature".into(), committer: None, message: String::new(), mainline: 0, })) .await .unwrap(); let blob = svc .get_blob(tonic::Request::new(GetBlobRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "feature".into(), })), }), path: "root_only.txt".into(), oid: None, max_bytes: 0, })) .await .unwrap() .into_inner(); assert_eq!(blob.data, b"from root\n"); } #[tokio::test] async fn test_revert_commit() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let created = svc .create_commit(tonic::Request::new(CreateCommitRequest { repository: Some(hdr()), branch: "main".into(), message: "to be reverted".into(), author: None, committer: None, actions: vec![CreateCommitAction { action: create_commit_action::Action::CreateCommitActionCreate as i32, file_path: "revert_me.txt".into(), previous_path: String::new(), content: b"will be reverted".to_vec(), encoding: String::new(), executable: false, last_commit_oid: None, }], start_revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), force: false, trailers: vec![], })) .await .unwrap() .into_inner(); let to_revert = created .commit .as_ref() .unwrap() .oid .as_ref() .unwrap() .hex .clone(); let revert_result = svc .revert_commit(tonic::Request::new(RevertCommitRequest { repository: Some(hdr()), commit: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: to_revert, })), }), branch: "main".into(), committer: None, message: String::new(), })) .await .unwrap() .into_inner(); let revert_commit = revert_result.commit.unwrap(); assert!( revert_commit.subject.starts_with("Revert"), "subject should start with 'Revert', got: {}", revert_commit.subject ); let blob_result = svc .get_blob(tonic::Request::new(GetBlobRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), path: "revert_me.txt".into(), oid: None, max_bytes: 0, })) .await; assert!( blob_result.is_err(), "revert_me.txt should be deleted after revert" ); } #[tokio::test] async fn test_oid_binary_encoding() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let commit = svc .get_commit(tonic::Request::new(GetCommitRequest { repository: Some(hdr()), revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), include_stats: false, include_raw: false, })) .await .unwrap() .into_inner(); let oid = commit.oid.unwrap(); assert_eq!(oid.value.len(), 20); assert_eq!(oid.hex.len(), 40); let hex_from_bytes: String = oid.value.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!(hex_from_bytes, oid.hex); } #[test] fn test_count_commits_head() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.count_commits(CountCommitsRequest { repository: Some(hdr()), revision: String::new(), path: String::new(), since: String::new(), until: String::new(), }).unwrap(); assert_eq!(resp.count, 4); } #[test] fn test_count_commits_with_revision() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.count_commits(CountCommitsRequest { repository: Some(hdr()), revision: "feature".into(), path: String::new(), since: String::new(), until: String::new(), }).unwrap(); assert_eq!(resp.count, 1); } #[test] fn test_count_commits_with_path() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.count_commits(CountCommitsRequest { repository: Some(hdr()), revision: "main".into(), path: "README.md".into(), since: String::new(), until: String::new(), }).unwrap(); assert!(resp.count >= 1); } #[test] fn test_count_diverging_commits() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.count_diverging_commits(CountDivergingCommitsRequest { repository: Some(hdr()), left: "feature".into(), right: "main".into(), }).unwrap(); assert_eq!(resp.left_count, 0); assert_eq!(resp.right_count, 3); } #[test] fn test_find_commit_by_oid() { let (_dir, gb) = common::setup_bare_repo(); let oid = common::get_main_oid(&gb); let commit = gb.find_commit(FindCommitRequest { repository: Some(hdr()), revision: common::oid_selector(&oid), include_stats: false, }).unwrap(); assert!(!commit.oid.as_ref().unwrap().hex.is_empty()); } #[test] fn test_find_commit_by_revision() { let (_dir, gb) = common::setup_bare_repo(); let commit = gb.find_commit(FindCommitRequest { repository: Some(hdr()), revision: common::rev_selector("main"), include_stats: false, }).unwrap(); assert!(!commit.oid.as_ref().unwrap().hex.is_empty()); } #[test] fn test_find_commit_default_head() { let (_dir, gb) = common::setup_bare_repo(); let commit = gb.find_commit(FindCommitRequest { repository: Some(hdr()), revision: None, include_stats: false, }).unwrap(); assert!(!commit.oid.as_ref().unwrap().hex.is_empty()); } #[test] fn test_list_commits_by_oid() { let (_dir, gb) = common::setup_bare_repo(); let oid = common::get_main_oid(&gb); let oid_bytes = gitks::oid::hex_to_bytes(&oid).unwrap(); let resp = gb.list_commits_by_oid(ListCommitsByOidRequest { repository: Some(hdr()), oids: vec![oid_bytes], include_stats: false, }).unwrap(); assert_eq!(resp.commits.len(), 1); } #[test] fn test_list_commits_by_oid_empty() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.list_commits_by_oid(ListCommitsByOidRequest { repository: Some(hdr()), oids: vec![], include_stats: false, }).unwrap(); assert!(resp.commits.is_empty()); } #[test] fn test_commits_by_message_basic() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.commits_by_message(CommitsByMessageRequest { repository: Some(hdr()), query: "initial".into(), revision: String::new(), limit: 10, offset: 0, case_insensitive: false, }).unwrap(); assert_eq!(resp.commits.len(), 1); } #[test] fn test_commits_by_message_case_insensitive() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.commits_by_message(CommitsByMessageRequest { repository: Some(hdr()), query: "INITIAL".into(), revision: String::new(), limit: 10, offset: 0, case_insensitive: true, }).unwrap(); assert_eq!(resp.commits.len(), 1); } #[test] fn test_commits_by_message_no_match() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.commits_by_message(CommitsByMessageRequest { repository: Some(hdr()), query: "zzzznonexistent".into(), revision: String::new(), limit: 10, offset: 0, case_insensitive: false, }).unwrap(); assert!(resp.commits.is_empty()); } #[test] fn test_check_objects_exist() { let (_dir, gb) = common::setup_bare_repo(); let oid = common::get_main_oid(&gb); let resp = gb.check_objects_exist(CheckObjectsExistRequest { repository: Some(hdr()), revisions: vec![oid.clone(), "HEAD".into(), "nonexistent-branch".into()], }).unwrap(); assert_eq!(resp.revisions.len(), 3); assert!(resp.revisions[0].exists); assert!(resp.revisions[1].exists); assert!(!resp.revisions[2].exists); } #[test] fn test_get_commit_stats() { let (_dir, gb) = common::setup_bare_repo(); let oid = common::get_main_oid(&gb); let stats = gb.get_commit_stats(GetCommitStatsRequest { repository: Some(hdr()), revision: common::oid_selector(&oid), }).unwrap(); assert!(stats.changed_files >= 1); } #[test] fn test_get_commit_stats_default() { let (_dir, gb) = common::setup_bare_repo(); let stats = gb.get_commit_stats(GetCommitStatsRequest { repository: Some(hdr()), revision: None, }).unwrap(); assert!(stats.changed_files >= 1); } #[test] fn test_last_commit_for_path() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.last_commit_for_path(LastCommitForPathRequest { repository: Some(hdr()), path: "README.md".into(), revision: "main".into(), literal_pathspec: false, }).unwrap(); assert!(resp.commit.is_some()); assert_eq!(resp.path, "README.md"); } #[test] fn test_last_commit_for_path_nonexistent() { let (_dir, gb) = common::setup_bare_repo(); let resp = gb.last_commit_for_path(LastCommitForPathRequest { repository: Some(hdr()), path: "nonexistent.txt".into(), revision: "main".into(), literal_pathspec: false, }).unwrap(); assert!(resp.commit.is_none()); }