mod common; use gitks::pb::*; #[test] fn test_get_commit_with_author() { let (_dir, gb) = common::setup_bare_repo(); let commit = gb .get_commit(GetCommitRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), include_stats: false, include_raw: false, }) .expect("get_commit"); 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" ); } #[test] fn test_get_commit_subject_body() { let (_dir, gb) = common::setup_bare_repo(); let commit = gb .get_commit(GetCommitRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main~2".into(), })), }), include_stats: false, include_raw: false, }) .expect("get_commit"); assert_eq!(commit.subject, "second commit"); assert!(!commit.message.is_empty()); assert!(commit.oid.is_some()); assert!(!commit.parent_oids.is_empty()); } #[test] fn test_get_commit_with_raw() { let (_dir, gb) = common::setup_bare_repo(); let commit = gb .get_commit(GetCommitRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), include_stats: false, include_raw: true, }) .expect("get_commit with raw"); 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"); } #[test] fn test_list_commits_with_pagination() { let (_dir, gb) = common::setup_bare_repo(); let page1 = gb .list_commits(ListCommitsRequest { repository: None, 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(), }), }) .expect("list_commits page 1"); assert_eq!(page1.commits.len(), 2); let pi = page1.page_info.unwrap(); assert!(pi.has_next_page); let page2 = gb .list_commits(ListCommitsRequest { repository: None, 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, }), }) .expect("list_commits page 2"); assert!(!page2.commits.is_empty()); assert_ne!(page1.commits[0].oid, page2.commits[0].oid); } #[test] fn test_get_commit_ancestors_pagination() { let (_dir, gb) = common::setup_bare_repo(); let page1 = gb .get_commit_ancestors(GetCommitAncestorsRequest { repository: None, 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(), }), }) .expect("ancestors page 1"); 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 = gb .get_commit_ancestors(GetCommitAncestorsRequest { repository: None, 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, }), }) .expect("ancestors page 2"); 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, ); } #[test] fn test_compare_commits() { let (_dir, gb) = common::setup_bare_repo(); let result = gb .compare_commits(CompareCommitsRequest { repository: None, 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(), }), }) .expect("compare_commits"); assert!(!result.commits.is_empty()); assert!(result.merge_base.is_some()); let stats = result.stats.unwrap(); assert!(stats.additions > 0); } #[test] fn test_create_commit_and_cherry_pick() { let (_dir, gb) = common::setup_bare_repo(); let created = gb .create_commit(CreateCommitRequest { repository: None, 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![], }) .expect("create_commit for cherry-pick source"); let source_oid = created .commit .as_ref() .unwrap() .oid .as_ref() .unwrap() .hex .clone(); let cp_result = gb .cherry_pick_commit(CherryPickCommitRequest { repository: None, 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, }) .expect("cherry_pick_commit"); let cp_commit = cp_result.commit.unwrap(); assert_eq!(cp_commit.subject, "cherry-pick source"); let blob = gb .get_blob(GetBlobRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), path: "cp_file.txt".into(), oid: None, max_bytes: 0, }) .expect("get_blob after cherry-pick"); assert_eq!(blob.data, b"cherry pick me"); } #[test] fn test_cherry_pick_root_commit() { let (dir, gb) = common::setup_bare_repo(); 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(); gb.cherry_pick_commit(CherryPickCommitRequest { repository: None, commit: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: root_oid, })), }), branch: "feature".into(), committer: None, message: String::new(), mainline: 0, }) .expect("cherry_pick_commit root"); let blob = gb .get_blob(GetBlobRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "feature".into(), })), }), path: "root_only.txt".into(), oid: None, max_bytes: 0, }) .expect("get root file after cherry-pick"); assert_eq!(blob.data, b"from root\n"); } #[test] fn test_revert_commit() { let (_dir, gb) = common::setup_bare_repo(); let created = gb .create_commit(CreateCommitRequest { repository: None, 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![], }) .expect("create_commit"); let to_revert = created .commit .as_ref() .unwrap() .oid .as_ref() .unwrap() .hex .clone(); let revert_result = gb .revert_commit(RevertCommitRequest { repository: None, commit: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: to_revert, })), }), branch: "main".into(), committer: None, message: String::new(), }) .expect("revert_commit"); 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 = gb.get_blob(GetBlobRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), path: "revert_me.txt".into(), oid: None, max_bytes: 0, }); assert!( blob_result.is_err(), "revert_me.txt should be deleted after revert" ); } #[test] fn test_oid_binary_encoding() { let (_dir, gb) = common::setup_bare_repo(); let commit = gb .get_commit(GetCommitRequest { repository: None, revision: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), include_stats: false, include_raw: false, }) .expect("get_commit"); 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); }