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,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");
|
||||
}
|
||||
Reference in New Issue
Block a user