feat(core): implement Git repository operations with gRPC services
- Add advertise_refs functionality for Git protocol communication - Implement archive service with TAR/ZIP format support and streaming - Create blame service for Git file annotation with line tracking - Add branch management including create, delete, rename and compare operations - Implement merge checking with conflict detection and fast-forward handling - Add cherry-pick functionality for applying commits between branches - Integrate gix library for Git repository operations and object handling - Add comprehensive test suite covering all Git operations - Implement proper error handling and repository validation - Add pagination support for large result sets - Create protobuf definitions for all Git operations and data structures - Add build system for gRPC code generation and dependency management
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_archive_tar() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let chunks = gb
|
||||
.get_archive(ArchiveRequest {
|
||||
repository: None,
|
||||
treeish: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: Some(ArchiveOptions {
|
||||
format: archive_options::Format::ArchiveFormatTar as i32,
|
||||
..Default::default()
|
||||
}),
|
||||
})
|
||||
.expect("get_archive tar");
|
||||
|
||||
assert!(!chunks.is_empty(), "should produce archive data");
|
||||
let total_size: usize = chunks.iter().map(|c| c.data.len()).sum();
|
||||
assert!(total_size > 0, "archive should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_archive_zip() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let chunks = gb
|
||||
.get_archive(ArchiveRequest {
|
||||
repository: None,
|
||||
treeish: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: Some(ArchiveOptions {
|
||||
format: archive_options::Format::ArchiveFormatZip as i32,
|
||||
..Default::default()
|
||||
}),
|
||||
})
|
||||
.expect("get_archive zip");
|
||||
|
||||
assert!(!chunks.is_empty());
|
||||
let data = &chunks[0].data;
|
||||
assert!(
|
||||
data.starts_with(b"PK"),
|
||||
"zip archive should start with PK magic bytes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_archive_entries() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.list_archive_entries(ListArchiveEntriesRequest {
|
||||
repository: None,
|
||||
treeish: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
pathspec: vec![],
|
||||
pagination: None,
|
||||
})
|
||||
.expect("list_archive_entries");
|
||||
|
||||
assert!(!result.entries.is_empty(), "should list entries");
|
||||
let paths: Vec<&str> = result.entries.iter().map(|e| e.path.as_str()).collect();
|
||||
assert!(
|
||||
paths.iter().any(|p| p.contains("README.md")),
|
||||
"should include README.md, got: {:?}",
|
||||
paths
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_archive_with_prefix() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let chunks = gb
|
||||
.get_archive(ArchiveRequest {
|
||||
repository: None,
|
||||
treeish: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: Some(ArchiveOptions {
|
||||
format: archive_options::Format::ArchiveFormatTar as i32,
|
||||
prefix: "project/".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
})
|
||||
.expect("get_archive with prefix");
|
||||
|
||||
assert!(!chunks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fsck_clean_repo() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.fsck(FsckRequest {
|
||||
repository: None,
|
||||
strict: false,
|
||||
connectivity_only: false,
|
||||
})
|
||||
.expect("fsck");
|
||||
assert!(result.ok);
|
||||
assert!(result.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_packfiles() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
|
||||
duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
gb.bare_dir.to_string_lossy().as_ref(),
|
||||
"gc",
|
||||
"--aggressive",
|
||||
],
|
||||
)
|
||||
.run()
|
||||
.expect("git gc");
|
||||
|
||||
let result = gb
|
||||
.list_packfiles(ListPackfilesRequest {
|
||||
repository: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("list_packfiles");
|
||||
|
||||
assert!(
|
||||
!result.packfiles.is_empty(),
|
||||
"bare repo should have packfiles after gc"
|
||||
);
|
||||
for pf in &result.packfiles {
|
||||
assert!(pf.size_bytes > 0, "packfile should have size");
|
||||
assert!(pf.name.ends_with(".pack"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_advertise_refs() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.advertise_refs(AdvertiseRefsRequest {
|
||||
repository: None,
|
||||
protocol: None,
|
||||
service: String::new(),
|
||||
})
|
||||
.expect("advertise_refs");
|
||||
|
||||
assert!(!result.references.is_empty(), "should have refs");
|
||||
let ref_names: Vec<&str> = result.references.iter().map(|r| r.name.as_str()).collect();
|
||||
assert!(
|
||||
ref_names.iter().any(|n| n.contains("refs/heads/main")),
|
||||
"should include main branch ref"
|
||||
);
|
||||
assert!(
|
||||
!result.capabilities.is_empty(),
|
||||
"should advertise capabilities"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_blame_basic() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.blame(BlameRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
range: None,
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("blame");
|
||||
|
||||
assert!(!result.hunks.is_empty(), "should have blame hunks");
|
||||
for hunk in &result.hunks {
|
||||
assert!(hunk.commit.is_some(), "each hunk should have a commit");
|
||||
let commit = hunk.commit.as_ref().unwrap();
|
||||
assert!(commit.oid.is_some(), "commit should have oid");
|
||||
assert!(!commit.oid.as_ref().unwrap().hex.is_empty());
|
||||
assert!(hunk.line_count > 0, "hunk should have lines");
|
||||
assert!(!hunk.lines.is_empty(), "hunk should have parsed lines");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blame_line_content() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.blame(BlameRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
range: None,
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("blame");
|
||||
|
||||
let all_lines: Vec<String> = result
|
||||
.hunks
|
||||
.iter()
|
||||
.flat_map(|h| h.lines.iter())
|
||||
.map(|l| String::from_utf8_lossy(&l.content).to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
all_lines.iter().any(|l| l.contains("# Test")),
|
||||
"should contain file content, got: {:?}",
|
||||
all_lines
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blame_with_range() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.blame(BlameRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
range: Some(LineRange { start: 1, end: 1 }),
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("blame with range");
|
||||
|
||||
assert!(!result.hunks.is_empty(), "should have hunks for range");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blame_author_info() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.blame(BlameRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
range: None,
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("blame");
|
||||
|
||||
let hunk = &result.hunks[0];
|
||||
let commit = hunk.commit.as_ref().unwrap();
|
||||
if let Some(ref author) = commit.author {
|
||||
if let Some(ref id) = author.identity {
|
||||
assert_eq!(id.name, "Test", "author name should match");
|
||||
assert_eq!(id.email, "test@example.com");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blame_nonexistent_file() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb.blame(BlameRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "nonexistent.txt".into(),
|
||||
range: None,
|
||||
options: None,
|
||||
pagination: None,
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "blame on nonexistent file should fail");
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_branches() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: false,
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches");
|
||||
let names: Vec<String> = result.branches.iter().map(|b| b.name.clone()).collect();
|
||||
assert!(names.contains(&"feature".to_string()));
|
||||
assert!(names.contains(&"main".to_string()));
|
||||
assert!(result.branches.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_branches_merged_filter() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
|
||||
let merged = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: true,
|
||||
not_merged_into_head: false,
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches merged");
|
||||
|
||||
let not_merged = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: true,
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches not merged");
|
||||
|
||||
let merged_names: Vec<&str> = merged.branches.iter().map(|b| b.name.as_str()).collect();
|
||||
let not_merged_names: Vec<&str> = not_merged
|
||||
.branches
|
||||
.iter()
|
||||
.map(|b| b.name.as_str())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
merged_names.contains(&"main"),
|
||||
"main should be merged into HEAD, got: {:?}",
|
||||
merged_names
|
||||
);
|
||||
assert!(
|
||||
not_merged_names.contains(&"feature"),
|
||||
"feature should NOT be merged into HEAD, got: {:?}",
|
||||
not_merged_names
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_branch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let branch = gb
|
||||
.get_branch(GetBranchRequest {
|
||||
repository: None,
|
||||
name: "feature".into(),
|
||||
})
|
||||
.expect("get_branch");
|
||||
assert_eq!(branch.full_ref, "refs/heads/feature");
|
||||
let oid = branch.target_oid.unwrap();
|
||||
assert!(!oid.value.is_empty());
|
||||
assert_eq!(oid.value.len(), 20);
|
||||
assert_eq!(oid.hex.len(), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branch_pagination() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: false,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches page 1");
|
||||
let page_info = result.page_info.unwrap();
|
||||
assert_eq!(result.branches.len(), 1);
|
||||
assert!(page_info.has_next_page);
|
||||
|
||||
let result2 = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: false,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: page_info.next_page_token,
|
||||
}),
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches page 2");
|
||||
assert!(!result2.branches.is_empty());
|
||||
assert_ne!(result.branches[0].name, result2.branches[0].name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_and_delete_branch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let branch = gb
|
||||
.create_branch(CreateBranchRequest {
|
||||
repository: None,
|
||||
name: "new-branch".into(),
|
||||
start_point: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
force: false,
|
||||
})
|
||||
.expect("create_branch");
|
||||
assert_eq!(branch.name, "new-branch");
|
||||
|
||||
gb.delete_branch(DeleteBranchRequest {
|
||||
repository: None,
|
||||
name: "new-branch".into(),
|
||||
force: true,
|
||||
})
|
||||
.expect("delete_branch");
|
||||
|
||||
let result = gb.get_branch(GetBranchRequest {
|
||||
repository: None,
|
||||
name: "new-branch".into(),
|
||||
});
|
||||
assert!(result.is_err(), "deleted branch should not exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_branch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
gb.create_branch(CreateBranchRequest {
|
||||
repository: None,
|
||||
name: "to-rename".into(),
|
||||
start_point: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
force: false,
|
||||
})
|
||||
.expect("create branch for rename");
|
||||
|
||||
let renamed = gb
|
||||
.rename_branch(RenameBranchRequest {
|
||||
repository: None,
|
||||
old_name: "to-rename".into(),
|
||||
new_name: "renamed".into(),
|
||||
})
|
||||
.expect("rename_branch");
|
||||
assert_eq!(renamed.name, "renamed");
|
||||
|
||||
let old = gb.get_branch(GetBranchRequest {
|
||||
repository: None,
|
||||
name: "to-rename".into(),
|
||||
});
|
||||
assert!(old.is_err(), "old branch name should not exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_branch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.compare_branch(CompareBranchRequest {
|
||||
repository: None,
|
||||
source_branch: "feature".into(),
|
||||
target_branch: "main".into(),
|
||||
})
|
||||
.expect("compare_branch");
|
||||
|
||||
assert!(
|
||||
result.ahead_by > 0 || result.behind_by > 0,
|
||||
"branches should differ"
|
||||
);
|
||||
assert!(result.merge_base.is_some(), "should find merge base");
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use gitks::bare::GitBare;
|
||||
|
||||
pub fn run_git(work_dir: &std::path::Path, args: &[&str]) -> duct::Expression {
|
||||
duct::cmd("git", {
|
||||
let work_str = work_dir.to_string_lossy().into_owned();
|
||||
let mut v: Vec<String> = vec!["-C".into(), work_str];
|
||||
v.extend(args.iter().map(|s| s.to_string()));
|
||||
v
|
||||
})
|
||||
.env("GIT_AUTHOR_NAME", "Test")
|
||||
.env("GIT_AUTHOR_EMAIL", "test@example.com")
|
||||
.env("GIT_COMMITTER_NAME", "Test")
|
||||
.env("GIT_COMMITTER_EMAIL", "test@example.com")
|
||||
}
|
||||
|
||||
pub fn run(work_dir: &std::path::Path, args: &[&str]) {
|
||||
let result = run_git(work_dir, args)
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"git {} failed: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setup_bare_repo() -> (tempfile::TempDir, GitBare) {
|
||||
let dir = tempfile::tempdir().expect("create temp dir");
|
||||
let bare_dir = dir.path().join("test-repo");
|
||||
|
||||
duct::cmd(
|
||||
"git",
|
||||
["init", "--bare", bare_dir.to_string_lossy().as_ref()],
|
||||
)
|
||||
.run()
|
||||
.expect("git init --bare");
|
||||
|
||||
let work_dir = dir.path().join("work");
|
||||
duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
bare_dir.to_string_lossy().as_ref(),
|
||||
work_dir.to_string_lossy().as_ref(),
|
||||
],
|
||||
)
|
||||
.run()
|
||||
.expect("clone");
|
||||
|
||||
run(&work_dir, &["checkout", "-b", "main"]);
|
||||
|
||||
std::fs::write(work_dir.join("README.md"), "# Test\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "initial commit"]);
|
||||
|
||||
run(&work_dir, &["branch", "feature"]);
|
||||
|
||||
std::fs::write(work_dir.join("src.txt"), "source\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "second commit"]);
|
||||
|
||||
std::fs::write(work_dir.join("README.md"), "# Test\n\nUpdated.\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "third commit"]);
|
||||
|
||||
std::fs::create_dir_all(work_dir.join("src/lib")).unwrap();
|
||||
std::fs::write(work_dir.join("src/lib/mod.rs"), "pub fn hello() {}\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "add nested file"]);
|
||||
|
||||
run(&work_dir, &["tag", "v0.1.0"]);
|
||||
|
||||
run(
|
||||
&work_dir,
|
||||
&["push", "-f", "origin", "main:main", "feature:feature"],
|
||||
);
|
||||
run(
|
||||
&work_dir,
|
||||
&["push", "-f", "origin", "refs/tags/v0.1.0:refs/tags/v0.1.0"],
|
||||
);
|
||||
|
||||
duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
bare_dir.to_string_lossy().as_ref(),
|
||||
"symbolic-ref",
|
||||
"HEAD",
|
||||
"refs/heads/main",
|
||||
],
|
||||
)
|
||||
.run()
|
||||
.expect("set HEAD to main");
|
||||
|
||||
(dir, GitBare { bare_dir })
|
||||
}
|
||||
|
||||
pub fn setup_bare_repo_with_conflict() -> (tempfile::TempDir, GitBare) {
|
||||
let dir = tempfile::tempdir().expect("create temp dir");
|
||||
let bare_dir = dir.path().join("test-repo");
|
||||
|
||||
duct::cmd(
|
||||
"git",
|
||||
["init", "--bare", bare_dir.to_string_lossy().as_ref()],
|
||||
)
|
||||
.run()
|
||||
.expect("git init --bare");
|
||||
|
||||
let work_dir = dir.path().join("work");
|
||||
duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
bare_dir.to_string_lossy().as_ref(),
|
||||
work_dir.to_string_lossy().as_ref(),
|
||||
],
|
||||
)
|
||||
.run()
|
||||
.expect("clone");
|
||||
|
||||
run(&work_dir, &["checkout", "-b", "main"]);
|
||||
std::fs::write(work_dir.join("file.txt"), "base content\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "base commit"]);
|
||||
|
||||
run(&work_dir, &["checkout", "-b", "branch-a"]);
|
||||
std::fs::write(work_dir.join("file.txt"), "branch A content\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "branch A change"]);
|
||||
|
||||
run(&work_dir, &["checkout", "main"]);
|
||||
run(&work_dir, &["checkout", "-b", "branch-b"]);
|
||||
std::fs::write(work_dir.join("file.txt"), "branch B content\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "branch B change"]);
|
||||
|
||||
run(
|
||||
&work_dir,
|
||||
&[
|
||||
"push",
|
||||
"-f",
|
||||
"origin",
|
||||
"main:main",
|
||||
"branch-a:branch-a",
|
||||
"branch-b:branch-b",
|
||||
],
|
||||
);
|
||||
|
||||
duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
bare_dir.to_string_lossy().as_ref(),
|
||||
"symbolic-ref",
|
||||
"HEAD",
|
||||
"refs/heads/main",
|
||||
],
|
||||
)
|
||||
.run()
|
||||
.expect("set HEAD to main");
|
||||
|
||||
(dir, GitBare { bare_dir })
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_diff() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.get_diff(GetDiffRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~3".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_diff");
|
||||
|
||||
assert!(!result.files.is_empty());
|
||||
let paths: Vec<&str> = result.files.iter().map(|f| f.new_path.as_str()).collect();
|
||||
assert!(
|
||||
paths.iter().any(|p| p.contains("src.txt")),
|
||||
"should include src.txt, got: {:?}",
|
||||
paths
|
||||
);
|
||||
let stats = result.stats.unwrap();
|
||||
assert!(stats.additions > 0 || stats.changed_files > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_diff_with_patch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.get_diff(GetDiffRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~1".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: Some(DiffOptions {
|
||||
include_patch: true,
|
||||
context_lines: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_diff with patch");
|
||||
|
||||
assert!(!result.files.is_empty());
|
||||
for file in &result.files {
|
||||
if !file.binary {
|
||||
assert!(
|
||||
!file.patch.is_empty(),
|
||||
"non-binary file should have patch: {}",
|
||||
file.new_path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_diff_with_rename_detection() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
|
||||
gb.create_commit(CreateCommitRequest {
|
||||
repository: None,
|
||||
branch: "main".into(),
|
||||
message: "rename file".into(),
|
||||
author: None,
|
||||
committer: None,
|
||||
actions: vec![
|
||||
CreateCommitAction {
|
||||
action: create_commit_action::Action::CreateCommitActionCreate as i32,
|
||||
file_path: "renamed.txt".into(),
|
||||
previous_path: String::new(),
|
||||
content: b"source\n".to_vec(),
|
||||
encoding: String::new(),
|
||||
executable: false,
|
||||
last_commit_oid: None,
|
||||
},
|
||||
CreateCommitAction {
|
||||
action: create_commit_action::Action::CreateCommitActionDelete as i32,
|
||||
file_path: "src.txt".into(),
|
||||
previous_path: String::new(),
|
||||
content: 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 rename commit");
|
||||
|
||||
let result = gb
|
||||
.get_diff(GetDiffRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~1".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: Some(DiffOptions {
|
||||
rename_detection: true,
|
||||
..Default::default()
|
||||
}),
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_diff with rename detection");
|
||||
|
||||
let has_rename = result
|
||||
.files
|
||||
.iter()
|
||||
.any(|f| f.change_type == diff_file::ChangeType::DiffFileChangeTypeRenamed as i32);
|
||||
assert!(
|
||||
has_rename,
|
||||
"should detect rename, files: {:?}",
|
||||
result.files.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_commit_diff_root() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let commits = 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: true,
|
||||
max_parents: 0,
|
||||
min_parents: 0,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
})
|
||||
.expect("list_commits for root");
|
||||
let root_oid = commits.commits[0].oid.as_ref().unwrap().hex.clone();
|
||||
|
||||
let result = gb
|
||||
.get_commit_diff(GetCommitDiffRequest {
|
||||
repository: None,
|
||||
commit: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: root_oid,
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_commit_diff on root");
|
||||
|
||||
assert!(!result.files.is_empty(), "root commit should have files");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_diff_stats() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let stats = gb
|
||||
.get_diff_stats(GetDiffStatsRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~3".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("get_diff_stats");
|
||||
assert!(stats.additions > 0 || stats.changed_files > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_patch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let patches = gb
|
||||
.get_patch(GetPatchRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~1".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("get_patch");
|
||||
assert!(!patches.is_empty());
|
||||
let combined: String = patches
|
||||
.iter()
|
||||
.map(|p| String::from_utf8_lossy(&p.data).to_string())
|
||||
.collect();
|
||||
assert!(combined.contains("diff --git") || combined.contains("@@"));
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
use gitks::bare::GitBare;
|
||||
use gitks::pb::{
|
||||
CreateCommitAction, CreateCommitRequest, FindFilesRequest, FsckRequest, GetBlobRequest,
|
||||
GetBranchRequest, GetCommitDiffRequest, GetCommitRequest, GetDiffRequest, GetDiffStatsRequest,
|
||||
GetPatchRequest, GetTreeRequest, ListBranchesRequest, ListCommitsRequest, ListTagsRequest,
|
||||
ListTreeRequest, ObjectName, ObjectSelector, Pagination, create_commit_action, object_selector,
|
||||
};
|
||||
|
||||
fn run_git(work_dir: &std::path::Path, args: &[&str]) -> duct::Expression {
|
||||
duct::cmd("git", {
|
||||
let work_str = work_dir.to_string_lossy().into_owned();
|
||||
let mut v: Vec<String> = vec!["-C".into(), work_str];
|
||||
v.extend(args.iter().map(|s| s.to_string()));
|
||||
v
|
||||
})
|
||||
.env("GIT_AUTHOR_NAME", "Test")
|
||||
.env("GIT_AUTHOR_EMAIL", "test@example.com")
|
||||
.env("GIT_COMMITTER_NAME", "Test")
|
||||
.env("GIT_COMMITTER_EMAIL", "test@example.com")
|
||||
}
|
||||
|
||||
fn run(work_dir: &std::path::Path, args: &[&str]) {
|
||||
let result = run_git(work_dir, args)
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"git {} failed: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a temporary bare repo with real git history.
|
||||
fn setup_bare_repo() -> (tempfile::TempDir, GitBare) {
|
||||
let dir = tempfile::tempdir().expect("create temp dir");
|
||||
let bare_dir = dir.path().join("test-repo");
|
||||
|
||||
duct::cmd(
|
||||
"git",
|
||||
["init", "--bare", bare_dir.to_string_lossy().as_ref()],
|
||||
)
|
||||
.run()
|
||||
.expect("git init --bare");
|
||||
|
||||
let work_dir = dir.path().join("work");
|
||||
duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
bare_dir.to_string_lossy().as_ref(),
|
||||
work_dir.to_string_lossy().as_ref(),
|
||||
],
|
||||
)
|
||||
.run()
|
||||
.expect("clone");
|
||||
|
||||
run(&work_dir, &["checkout", "-b", "main"]);
|
||||
|
||||
// Initial commit: README.md
|
||||
std::fs::write(work_dir.join("README.md"), "# Test\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "initial commit"]);
|
||||
|
||||
// Branch feature from initial
|
||||
run(&work_dir, &["branch", "feature"]);
|
||||
|
||||
// Second commit: add src.txt
|
||||
std::fs::write(work_dir.join("src.txt"), "source\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "second commit"]);
|
||||
|
||||
// Third commit: modify README.md
|
||||
std::fs::write(work_dir.join("README.md"), "# Test\n\nUpdated.\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "third commit"]);
|
||||
|
||||
// Create a subdirectory with nested file
|
||||
std::fs::create_dir_all(work_dir.join("src/lib")).unwrap();
|
||||
std::fs::write(work_dir.join("src/lib/mod.rs"), "pub fn hello() {}\n").unwrap();
|
||||
run(&work_dir, &["add", "."]);
|
||||
run(&work_dir, &["commit", "-m", "add nested file"]);
|
||||
|
||||
run(&work_dir, &["tag", "v0.1.0"]);
|
||||
|
||||
run(
|
||||
&work_dir,
|
||||
&["push", "-f", "origin", "main:main", "feature:feature"],
|
||||
);
|
||||
run(
|
||||
&work_dir,
|
||||
&["push", "-f", "origin", "refs/tags/v0.1.0:refs/tags/v0.1.0"],
|
||||
);
|
||||
|
||||
(
|
||||
dir,
|
||||
GitBare {
|
||||
bare_dir: bare_dir.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_branches() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: false,
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches");
|
||||
let names: Vec<String> = result.branches.iter().map(|b| b.name.clone()).collect();
|
||||
assert!(names.contains(&"feature".to_string()), "names: {names:?}");
|
||||
assert!(
|
||||
result.branches.len() >= 2,
|
||||
"got {} branches",
|
||||
result.branches.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_branch() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let branch = gb
|
||||
.get_branch(GetBranchRequest {
|
||||
repository: None,
|
||||
name: "feature".into(),
|
||||
})
|
||||
.expect("get_branch");
|
||||
assert_eq!(branch.full_ref, "refs/heads/feature");
|
||||
let oid = branch.target_oid.unwrap();
|
||||
assert!(!oid.value.is_empty(), "oid.value must be binary bytes");
|
||||
assert_eq!(oid.value.len(), 20, "SHA-1 binary is 20 bytes");
|
||||
assert_eq!(oid.hex.len(), 40, "SHA-1 hex is 40 chars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branch_pagination() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: false,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches");
|
||||
let page_info = result.page_info.unwrap();
|
||||
assert_eq!(result.branches.len(), 1);
|
||||
assert!(page_info.has_next_page);
|
||||
assert!(!page_info.next_page_token.is_empty());
|
||||
|
||||
// Fetch second page
|
||||
let result2 = gb
|
||||
.list_branches(ListBranchesRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
merged_into_head: false,
|
||||
not_merged_into_head: false,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: page_info.next_page_token,
|
||||
}),
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_branches page 2");
|
||||
assert!(!result2.branches.is_empty());
|
||||
assert_ne!(result.branches[0].name, result2.branches[0].name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_commits() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = 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: 100,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
})
|
||||
.expect("list_commits");
|
||||
assert!(
|
||||
result.commits.len() >= 4,
|
||||
"expected >=4 commits, got {}",
|
||||
result.commits.len()
|
||||
);
|
||||
// Oid binary encoding check
|
||||
let first = &result.commits[0];
|
||||
let oid = first.oid.as_ref().unwrap();
|
||||
assert_eq!(oid.value.len(), 20, "binary OID must be 20 bytes for SHA-1");
|
||||
assert_eq!(oid.hex.len(), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_commits_with_pagination() {
|
||||
let (_dir, gb) = 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());
|
||||
// Page 2 commits should differ from page 1
|
||||
assert_ne!(page1.commits[0].oid, page2.commits[0].oid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_commit() {
|
||||
let (_dir, gb) = 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.oid.is_some());
|
||||
assert_eq!(commit.subject, "add nested file");
|
||||
assert!(!commit.parent_oids.is_empty(), "should have parent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_diff() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.get_diff(GetDiffRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~3".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_diff");
|
||||
|
||||
assert!(!result.files.is_empty(), "diff should have changed files");
|
||||
|
||||
// Check that file paths are populated (not empty strings)
|
||||
let paths: Vec<&str> = result.files.iter().map(|f| f.new_path.as_str()).collect();
|
||||
let has_src = paths.iter().any(|p| p.contains("src.txt"));
|
||||
assert!(has_src, "should include src.txt in diff, got: {paths:?}");
|
||||
|
||||
// Stats should not all be zero
|
||||
let stats = result.stats.unwrap();
|
||||
assert!(
|
||||
stats.additions > 0 || stats.changed_files > 0,
|
||||
"stats should be non-zero: {stats:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_diff_with_patch() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.get_diff(GetDiffRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~1".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: Some(gitks::pb::DiffOptions {
|
||||
include_patch: true,
|
||||
context_lines: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_diff with patch");
|
||||
|
||||
assert!(!result.files.is_empty());
|
||||
for file in &result.files {
|
||||
if !file.binary {
|
||||
assert!(
|
||||
!file.patch.is_empty(),
|
||||
"non-binary file should have patch data: {}",
|
||||
file.new_path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_commit_diff_root() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
// Get the root commit (first commit on main)
|
||||
let commits = 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: true,
|
||||
max_parents: 0,
|
||||
min_parents: 0,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
})
|
||||
.expect("list_commits for root");
|
||||
let root_oid = commits.commits[0].oid.as_ref().unwrap().hex.clone();
|
||||
|
||||
// get_commit_diff on root commit should not fail
|
||||
let result = gb
|
||||
.get_commit_diff(GetCommitDiffRequest {
|
||||
repository: None,
|
||||
commit: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: root_oid,
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("get_commit_diff on root");
|
||||
|
||||
assert!(
|
||||
!result.files.is_empty(),
|
||||
"root commit diff should show added files"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_diff_stats() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let stats = gb
|
||||
.get_diff_stats(GetDiffStatsRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~3".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("get_diff_stats");
|
||||
assert!(
|
||||
stats.additions > 0 || stats.changed_files > 0,
|
||||
"stats should be non-zero: {stats:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_commits() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.compare_commits(gitks::pb::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");
|
||||
|
||||
// feature branched off after initial commit; main has 3 more commits
|
||||
assert!(
|
||||
!result.commits.is_empty(),
|
||||
"should find commits between feature and main"
|
||||
);
|
||||
assert!(result.merge_base.is_some(), "should find merge base");
|
||||
let stats = result.stats.unwrap();
|
||||
assert!(stats.additions > 0, "should have additions: {stats:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_commit_with_actions() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.create_commit(CreateCommitRequest {
|
||||
repository: None,
|
||||
branch: "main".into(),
|
||||
message: "created via API".into(),
|
||||
author: None,
|
||||
committer: None,
|
||||
actions: vec![CreateCommitAction {
|
||||
action: create_commit_action::Action::CreateCommitActionCreate as i32,
|
||||
file_path: "api_file.txt".into(),
|
||||
previous_path: String::new(),
|
||||
content: b"hello from api".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 commit = result.commit.unwrap();
|
||||
assert_eq!(commit.subject, "created via API");
|
||||
assert!(!commit.parent_oids.is_empty(), "should have parent");
|
||||
|
||||
// Verify the file was created
|
||||
let blob = gb
|
||||
.get_blob(GetBlobRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "api_file.txt".into(),
|
||||
oid: None,
|
||||
max_bytes: 0,
|
||||
})
|
||||
.expect("get_blob after create_commit");
|
||||
assert_eq!(blob.data, b"hello from api");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_commit_delete_action() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.create_commit(CreateCommitRequest {
|
||||
repository: None,
|
||||
branch: "main".into(),
|
||||
message: "delete src.txt".into(),
|
||||
author: None,
|
||||
committer: None,
|
||||
actions: vec![CreateCommitAction {
|
||||
action: create_commit_action::Action::CreateCommitActionDelete as i32,
|
||||
file_path: "src.txt".into(),
|
||||
previous_path: String::new(),
|
||||
content: 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 delete");
|
||||
|
||||
let commit = result.commit.unwrap();
|
||||
assert_eq!(commit.subject, "delete src.txt");
|
||||
|
||||
// Verify file is gone
|
||||
let blob_result = gb.get_blob(GetBlobRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "src.txt".into(),
|
||||
oid: None,
|
||||
max_bytes: 0,
|
||||
});
|
||||
assert!(blob_result.is_err(), "src.txt should be deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tree_recursive() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.list_tree(ListTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: String::new(),
|
||||
recursive: true,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("list_tree recursive");
|
||||
|
||||
let paths: Vec<String> = result.entries.iter().map(|e| e.path.clone()).collect();
|
||||
assert!(
|
||||
paths.iter().any(|p| p.contains("src/lib/mod.rs")),
|
||||
"recursive tree should include nested files, got: {paths:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tree_subpath() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.get_tree(GetTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "src".into(),
|
||||
})
|
||||
.expect("get_tree subpath");
|
||||
|
||||
// OID should be the src subtree, not the root tree
|
||||
assert!(result.oid.is_some());
|
||||
let root_tree = gb
|
||||
.get_tree(GetTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: String::new(),
|
||||
})
|
||||
.expect("get_tree root");
|
||||
assert_ne!(
|
||||
result.oid.unwrap().hex,
|
||||
root_tree.oid.unwrap().hex,
|
||||
"subtree OID should differ from root tree OID"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_files() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.find_files(FindFilesRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
pattern: "mod.rs".into(),
|
||||
pathspec: vec![],
|
||||
pagination: None,
|
||||
})
|
||||
.expect("find_files");
|
||||
|
||||
assert!(!result.files.is_empty(), "should find mod.rs files");
|
||||
assert!(
|
||||
result.files.iter().all(|f| f.path.contains("mod.rs")),
|
||||
"all results should match pattern"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tags() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.list_tags(ListTagsRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_tags");
|
||||
let names: Vec<String> = result.tags.iter().map(|t| t.name.clone()).collect();
|
||||
assert!(names.contains(&"v0.1.0".to_string()), "names: {names:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fsck_clean_repo() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let result = gb
|
||||
.fsck(FsckRequest {
|
||||
repository: None,
|
||||
strict: false,
|
||||
connectivity_only: false,
|
||||
})
|
||||
.expect("fsck");
|
||||
assert!(result.ok);
|
||||
assert!(result.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_patch() {
|
||||
let (_dir, gb) = setup_bare_repo();
|
||||
let patches = gb
|
||||
.get_patch(GetPatchRequest {
|
||||
repository: None,
|
||||
base: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main~1".into(),
|
||||
})),
|
||||
}),
|
||||
head: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("get_patch");
|
||||
assert!(!patches.is_empty());
|
||||
let combined: String = patches
|
||||
.iter()
|
||||
.map(|p| String::from_utf8_lossy(&p.data).to_string())
|
||||
.collect();
|
||||
assert!(
|
||||
combined.contains("diff --git") || combined.contains("@@"),
|
||||
"patch should contain diff output: {combined}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oid_binary_encoding() {
|
||||
let (_dir, gb) = 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();
|
||||
// Binary value should be raw bytes, not hex string bytes
|
||||
assert_eq!(
|
||||
oid.value.len(),
|
||||
20,
|
||||
"SHA-1 binary is 20 bytes, got {}",
|
||||
oid.value.len()
|
||||
);
|
||||
assert_eq!(oid.hex.len(), 40, "SHA-1 hex is 40 chars");
|
||||
// Verify hex and binary match
|
||||
let hex_from_bytes: String = oid.value.iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(hex_from_bytes, oid.hex, "binary and hex must match");
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_check_merge_no_conflict() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.check_merge(CheckMergeRequest {
|
||||
repository: None,
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "feature".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("check_merge");
|
||||
|
||||
assert!(
|
||||
result.status == merge_result::Status::MergeResultStatusMerged as i32
|
||||
|| result.status == merge_result::Status::MergeResultStatusFastForward as i32
|
||||
|| result.status == merge_result::Status::MergeResultStatusAlreadyUpToDate as i32,
|
||||
"merge should be clean, got status: {}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_merge_with_conflict() {
|
||||
let (_dir, gb) = common::setup_bare_repo_with_conflict();
|
||||
let result = gb
|
||||
.check_merge(CheckMergeRequest {
|
||||
repository: None,
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-a".into(),
|
||||
})),
|
||||
}),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-b".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("check_merge with conflict");
|
||||
|
||||
assert_eq!(
|
||||
result.status,
|
||||
merge_result::Status::MergeResultStatusConflicts as i32,
|
||||
"merge should have conflicts"
|
||||
);
|
||||
assert!(!result.conflicts.is_empty(), "should list conflicted files");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_merge_already_up_to_date() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.check_merge(CheckMergeRequest {
|
||||
repository: None,
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
options: None,
|
||||
})
|
||||
.expect("check_merge same ref");
|
||||
|
||||
assert_eq!(
|
||||
result.status,
|
||||
merge_result::Status::MergeResultStatusAlreadyUpToDate as i32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_fast_forward() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.merge(MergeRequest {
|
||||
repository: None,
|
||||
target_branch: "feature".into(),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
committer: None,
|
||||
message: String::new(),
|
||||
options: None,
|
||||
})
|
||||
.expect("merge fast-forward");
|
||||
|
||||
assert!(
|
||||
result.status == merge_result::Status::MergeResultStatusFastForward as i32
|
||||
|| result.status == merge_result::Status::MergeResultStatusAlreadyUpToDate as i32,
|
||||
"feature should fast-forward to main, got: {}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_with_conflict() {
|
||||
let (_dir, gb) = common::setup_bare_repo_with_conflict();
|
||||
let result = gb
|
||||
.merge(MergeRequest {
|
||||
repository: None,
|
||||
target_branch: "branch-a".into(),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-b".into(),
|
||||
})),
|
||||
}),
|
||||
committer: None,
|
||||
message: String::new(),
|
||||
options: None,
|
||||
})
|
||||
.expect("merge with conflict");
|
||||
|
||||
assert_eq!(
|
||||
result.status,
|
||||
merge_result::Status::MergeResultStatusConflicts as i32,
|
||||
"should detect conflicts"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_fast_forward_only_aborts_non_fast_forward() {
|
||||
let (_dir, gb) = common::setup_bare_repo_with_conflict();
|
||||
let result = gb
|
||||
.merge(MergeRequest {
|
||||
repository: None,
|
||||
target_branch: "branch-a".into(),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-b".into(),
|
||||
})),
|
||||
}),
|
||||
committer: None,
|
||||
message: String::new(),
|
||||
options: Some(MergeOptions {
|
||||
fast_forward: merge_options::FastForwardMode::MergeFastForwardModeOnly as i32,
|
||||
..Default::default()
|
||||
}),
|
||||
})
|
||||
.expect("merge fast-forward only");
|
||||
|
||||
assert_eq!(
|
||||
result.status,
|
||||
merge_result::Status::MergeResultStatusAborted as i32
|
||||
);
|
||||
assert!(result.commit.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_merge_conflicts() {
|
||||
let (_dir, gb) = common::setup_bare_repo_with_conflict();
|
||||
let result = gb
|
||||
.list_merge_conflicts(ListMergeConflictsRequest {
|
||||
repository: None,
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-a".into(),
|
||||
})),
|
||||
}),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-b".into(),
|
||||
})),
|
||||
}),
|
||||
pagination: None,
|
||||
})
|
||||
.expect("list_merge_conflicts");
|
||||
|
||||
assert!(!result.conflicts.is_empty(), "should list conflicted files");
|
||||
assert!(
|
||||
result.conflicts.iter().any(|c| c.path == "file.txt"),
|
||||
"file.txt should be conflicted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_merge_conflicts() {
|
||||
let (_dir, gb) = common::setup_bare_repo_with_conflict();
|
||||
|
||||
let result = gb
|
||||
.resolve_merge_conflicts(ResolveMergeConflictsRequest {
|
||||
repository: None,
|
||||
target_branch: "branch-a".into(),
|
||||
source: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-b".into(),
|
||||
})),
|
||||
}),
|
||||
resolutions: vec![ResolveMergeConflict {
|
||||
path: "file.txt".into(),
|
||||
content: b"resolved content\n".to_vec(),
|
||||
}],
|
||||
committer: None,
|
||||
message: "resolved conflicts".into(),
|
||||
})
|
||||
.expect("resolve_merge_conflicts");
|
||||
|
||||
assert_eq!(
|
||||
result.status,
|
||||
merge_result::Status::MergeResultStatusMerged as i32
|
||||
);
|
||||
assert!(result.commit.is_some());
|
||||
|
||||
let blob = gb
|
||||
.get_blob(GetBlobRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "branch-a".into(),
|
||||
})),
|
||||
}),
|
||||
path: "file.txt".into(),
|
||||
oid: None,
|
||||
max_bytes: 0,
|
||||
})
|
||||
.expect("get resolved blob");
|
||||
assert_eq!(String::from_utf8_lossy(&blob.data), "resolved content\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rebase() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
|
||||
gb.create_commit(CreateCommitRequest {
|
||||
repository: None,
|
||||
branch: "feature".into(),
|
||||
message: "feature work".into(),
|
||||
author: None,
|
||||
committer: None,
|
||||
actions: vec![CreateCommitAction {
|
||||
action: create_commit_action::Action::CreateCommitActionCreate as i32,
|
||||
file_path: "feature.txt".into(),
|
||||
previous_path: String::new(),
|
||||
content: b"feature content".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 feature commit");
|
||||
|
||||
let result = gb
|
||||
.rebase(RebaseRequest {
|
||||
repository: None,
|
||||
branch: "feature".into(),
|
||||
upstream: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
committer: None,
|
||||
})
|
||||
.expect("rebase");
|
||||
|
||||
assert_eq!(
|
||||
result.status,
|
||||
rebase_result::Status::RebaseResultStatusRebased as i32
|
||||
);
|
||||
assert!(result.head.is_some());
|
||||
|
||||
let blob = gb
|
||||
.get_blob(GetBlobRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "feature".into(),
|
||||
})),
|
||||
}),
|
||||
path: "feature.txt".into(),
|
||||
oid: None,
|
||||
max_bytes: 0,
|
||||
})
|
||||
.expect("get rebased feature file");
|
||||
assert_eq!(String::from_utf8_lossy(&blob.data), "feature content");
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_tags() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.list_tags(ListTagsRequest {
|
||||
repository: None,
|
||||
pattern: String::new(),
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_tags");
|
||||
let names: Vec<String> = result.tags.iter().map(|t| t.name.clone()).collect();
|
||||
assert!(names.contains(&"v0.1.0".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tag() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let tag = gb
|
||||
.get_tag(GetTagRequest {
|
||||
repository: None,
|
||||
name: "v0.1.0".into(),
|
||||
include_raw: false,
|
||||
})
|
||||
.expect("get_tag");
|
||||
|
||||
assert_eq!(tag.name, "v0.1.0");
|
||||
assert!(tag.target_oid.is_some());
|
||||
assert_eq!(tag.full_ref, "refs/tags/v0.1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_and_delete_lightweight_tag() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let tag = gb
|
||||
.create_tag(CreateTagRequest {
|
||||
repository: None,
|
||||
name: "v0.2.0".into(),
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
message: String::new(),
|
||||
tagger: None,
|
||||
force: false,
|
||||
annotated: false,
|
||||
})
|
||||
.expect("create_tag");
|
||||
|
||||
assert_eq!(tag.name, "v0.2.0");
|
||||
assert!(!tag.annotated);
|
||||
|
||||
gb.delete_tag(DeleteTagRequest {
|
||||
repository: None,
|
||||
name: "v0.2.0".into(),
|
||||
})
|
||||
.expect("delete_tag");
|
||||
|
||||
let result = gb.get_tag(GetTagRequest {
|
||||
repository: None,
|
||||
name: "v0.2.0".into(),
|
||||
include_raw: false,
|
||||
});
|
||||
assert!(result.is_err(), "deleted tag should not exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_annotated_tag() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let tag = gb
|
||||
.create_tag(CreateTagRequest {
|
||||
repository: None,
|
||||
name: "v1.0.0".into(),
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
message: "Release v1.0.0".into(),
|
||||
tagger: None,
|
||||
force: false,
|
||||
annotated: true,
|
||||
})
|
||||
.expect("create annotated tag");
|
||||
|
||||
assert_eq!(tag.name, "v1.0.0");
|
||||
assert!(tag.annotated, "should be annotated");
|
||||
assert!(tag.tag_oid.is_some(), "annotated tag should have tag_oid");
|
||||
assert!(
|
||||
tag.message.contains("Release v1.0.0"),
|
||||
"message should be set"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tags_with_pattern() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
|
||||
gb.create_tag(CreateTagRequest {
|
||||
repository: None,
|
||||
name: "release-1.0".into(),
|
||||
target: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
message: String::new(),
|
||||
tagger: None,
|
||||
force: false,
|
||||
annotated: false,
|
||||
})
|
||||
.expect("create release tag");
|
||||
|
||||
let result = gb
|
||||
.list_tags(ListTagsRequest {
|
||||
repository: None,
|
||||
pattern: "release".into(),
|
||||
pagination: None,
|
||||
sort_direction: 0,
|
||||
})
|
||||
.expect("list_tags with pattern");
|
||||
|
||||
assert!(
|
||||
result.tags.iter().all(|t| t.name.contains("release")),
|
||||
"all tags should match pattern"
|
||||
);
|
||||
assert!(!result.tags.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_tag() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.verify_tag(VerifyTagRequest {
|
||||
repository: None,
|
||||
name: "v0.1.0".into(),
|
||||
})
|
||||
.expect("verify_tag");
|
||||
|
||||
assert!(!result.verified, "unsigned tag should not be verified");
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
mod common;
|
||||
|
||||
use gitks::pb::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_tree_recursive() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.list_tree(ListTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: String::new(),
|
||||
recursive: true,
|
||||
pagination: None,
|
||||
})
|
||||
.expect("list_tree recursive");
|
||||
|
||||
let paths: Vec<String> = result.entries.iter().map(|e| e.path.clone()).collect();
|
||||
assert!(
|
||||
paths.iter().any(|p| p.contains("src/lib/mod.rs")),
|
||||
"should include nested files, got: {:?}",
|
||||
paths
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tree_subpath() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.get_tree(GetTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "src".into(),
|
||||
})
|
||||
.expect("get_tree subpath");
|
||||
|
||||
assert!(result.oid.is_some());
|
||||
let root_tree = gb
|
||||
.get_tree(GetTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: String::new(),
|
||||
})
|
||||
.expect("get_tree root");
|
||||
assert_ne!(
|
||||
result.oid.unwrap().hex,
|
||||
root_tree.oid.unwrap().hex,
|
||||
"subtree OID should differ from root"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_files() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.find_files(FindFilesRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
pattern: "mod.rs".into(),
|
||||
pathspec: vec![],
|
||||
pagination: None,
|
||||
})
|
||||
.expect("find_files");
|
||||
|
||||
assert!(!result.files.is_empty());
|
||||
assert!(result.files.iter().all(|f| f.path.contains("mod.rs")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_blob() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let blob = gb
|
||||
.get_blob(GetBlobRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
oid: None,
|
||||
max_bytes: 0,
|
||||
})
|
||||
.expect("get_blob");
|
||||
|
||||
let content = String::from_utf8_lossy(&blob.data);
|
||||
assert!(content.contains("# Test"));
|
||||
assert!(blob.size > 0);
|
||||
assert!(!blob.binary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_blob_with_truncation() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let blob = gb
|
||||
.get_blob(GetBlobRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
oid: None,
|
||||
max_bytes: 5,
|
||||
})
|
||||
.expect("get_blob truncated");
|
||||
|
||||
assert_eq!(blob.data.len(), 5);
|
||||
assert!(blob.truncated);
|
||||
assert!(
|
||||
blob.size > 5,
|
||||
"size should be original size, not truncated size"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_file_metadata() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let meta = gb
|
||||
.get_file_metadata(GetFileMetadataRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: "README.md".into(),
|
||||
})
|
||||
.expect("get_file_metadata");
|
||||
|
||||
assert_eq!(meta.path, "README.md");
|
||||
assert!(meta.oid.is_some());
|
||||
assert_eq!(meta.r#type, ObjectType::Blob as i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tree_with_pagination() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let result = gb
|
||||
.list_tree(ListTreeRequest {
|
||||
repository: None,
|
||||
revision: Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: "main".into(),
|
||||
})),
|
||||
}),
|
||||
path: String::new(),
|
||||
recursive: false,
|
||||
pagination: Some(Pagination {
|
||||
page_size: 1,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
})
|
||||
.expect("list_tree paginated");
|
||||
|
||||
assert_eq!(result.entries.len(), 1);
|
||||
let pi = result.page_info.unwrap();
|
||||
assert!(pi.has_next_page);
|
||||
}
|
||||
Reference in New Issue
Block a user