cc202d6d1f
- Add tracing spans with repo labels for archive and blame operations - Implement caching for archive list entries when using OID selectors - Implement caching for blame operations when using OID selectors - Add detailed
739 lines
23 KiB
Rust
739 lines
23 KiB
Rust
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::new(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");
|
|
}
|