makepad/libs/git/tests/local_clone_test.rs
Admin f08ef0a5e3 libs: git
Squash of 1 work commits (Sep 9–9):
  842ea96  git: topological log, worktree lifecycle and bounded line diff in libs/git

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 13:40:31 +02:00

220 lines
8.8 KiB
Rust

//! Local depth-1 clones with alternates, then commit and merge across the
//! clones — all on a repository built with the API, no git binary.
use makepad_git::test_support::{tempdir, write_pack};
use makepad_git::*;
use std::fs;
use std::path::Path;
fn sig() -> Signature {
Signature {
name: "Test".into(),
email: "info@makepad.nl".into(),
timestamp: 1700000000,
tz_offset: "+0000".into(),
}
}
fn init_repo(dir: &Path) -> Repository {
let git = dir.join(".git");
fs::create_dir_all(git.join("objects")).unwrap();
fs::create_dir_all(git.join("refs/heads")).unwrap();
fs::write(git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
Repository::open(dir).unwrap()
}
fn entry(name: &str, mode: u32, oid: ObjectId) -> TreeEntry {
TreeEntry {
mode,
name: name.into(),
oid,
}
}
/// A source repository: README.md, src/lib.rs (nested tree) and packed.txt
/// (stored only in a pack), committed on `main`.
fn make_source(dir: &Path) -> ObjectId {
let repo = init_repo(dir);
let readme = repo.write_blob(b"hello\n").unwrap();
let lib = repo.write_blob(b"fn x() {}\n").unwrap();
let packed = write_pack(&dir.join(".git"), &[(ObjectKind::Blob, b"packed\n".to_vec())]).unwrap()[0];
let src = repo
.write_tree(&Tree {
entries: vec![entry("lib.rs", 0o100644, lib)],
})
.unwrap();
let mut entries = vec![
entry("README.md", 0o100644, readme),
entry("packed.txt", 0o100644, packed),
entry("src", 0o040000, src),
];
entries.sort_by(|a, b| a.name.cmp(&b.name));
let root = repo.write_tree(&Tree { entries }).unwrap();
let commit = repo
.write_commit(&Commit {
tree: root,
parents: vec![],
author: sig(),
committer: sig(),
message: "initial\n".into(),
})
.unwrap();
repo.create_branch("main", &commit).unwrap();
commit
}
fn copy_loose_objects(src: &Path, dst: &Path) {
if let Ok(entries) = fs::read_dir(src) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.len() == 2 && entry.path().is_dir() {
let dst_sub = dst.join(&name);
let _ = fs::create_dir_all(&dst_sub);
if let Ok(sub_entries) = fs::read_dir(entry.path()) {
for sub in sub_entries.flatten() {
let dst_file = dst_sub.join(sub.file_name());
if !dst_file.exists() {
let _ = fs::copy(sub.path(), dst_file);
}
}
}
}
}
}
}
#[test]
fn test_local_clone_commit_merge() {
let base = tempdir().unwrap();
let root = base.path().join("source");
fs::create_dir_all(&root).unwrap();
let head = make_source(&root);
let checkout1 = base.path().join("checkout1");
let checkout2 = base.path().join("checkout2");
// --- Clone into checkout1 and checkout2 ---
let t1 = local_clone_depth1(&root, &checkout1, Some("main")).expect("clone checkout1 failed");
assert_eq!(t1.num_files, 3);
assert_eq!(fs::read_to_string(checkout1.join("README.md")).unwrap(), "hello\n");
assert_eq!(fs::read_to_string(checkout1.join("src/lib.rs")).unwrap(), "fn x() {}\n");
assert_eq!(fs::read_to_string(checkout1.join("packed.txt")).unwrap(), "packed\n");
assert!(t1.bytes_written > 0);
let alternates = fs::read_to_string(checkout1.join(".git/objects/info/alternates")).unwrap();
assert!(alternates.contains("source"), "alternates: {}", alternates);
let t2 = local_clone_depth1(&root, &checkout2, Some("main")).expect("clone checkout2 failed");
assert_eq!(t2.num_files, 3);
// --- Verify checkout1 with our own API ---
let mut repo1 = Repository::open(&checkout1).unwrap();
assert_eq!(repo1.head_oid().unwrap(), head);
assert_eq!(repo1.current_branch().unwrap().as_deref(), Some("main"));
assert_eq!(repo1.read_index().unwrap().entries.len(), 3);
let status = repo1.status().unwrap();
assert!(
!status.entries.iter().any(|e| matches!(
e.status,
FileStatus::Untracked | FileStatus::StagedNew | FileStatus::StagedDeleted | FileStatus::Deleted
)),
"working tree should be clean: {:?}",
status.entries
);
// HEAD is readable through the alternate
assert_eq!(repo1.read_commit(&head).unwrap().message, "initial\n");
// --- Make a commit in checkout2 ---
fs::write(checkout2.join("test_from_checkout2.txt"), "hello from checkout2\n").unwrap();
let mut repo2 = Repository::open(&checkout2).unwrap();
repo2.stage_file("test_from_checkout2.txt").unwrap();
let commit2_oid = repo2.commit("commit from checkout2\n", sig()).unwrap();
assert_eq!(repo2.head_oid().unwrap(), commit2_oid);
assert_eq!(repo2.read_commit(&commit2_oid).unwrap().parents, vec![head]);
// --- Merge checkout2 into checkout1 ---
// Copy new objects from checkout2 to checkout1 (just the loose ones from the commit)
copy_loose_objects(&checkout2.join(".git/objects"), &checkout1.join(".git/objects"));
// Create a branch in checkout1 for the merge source
refs::write_ref(&checkout1.join(".git"), "refs/heads/_from_checkout2", &commit2_oid).unwrap();
let mut repo1 = Repository::open(&checkout1).unwrap();
let result = repo1.merge_branch("_from_checkout2", sig()).unwrap();
assert!(!result.has_conflict(), "merge: {}", result.content());
// --- Verify merge result ---
assert!(checkout1.join("test_from_checkout2.txt").exists(), "merged file must exist");
assert_eq!(
fs::read_to_string(checkout1.join("test_from_checkout2.txt")).unwrap(),
"hello from checkout2\n"
);
let head1 = repo1.head_oid().unwrap();
// checkout1 had no commits of its own: a fast-forward to checkout2's commit
assert_eq!(head1, commit2_oid);
let log = repo1.log(&head1, 5).unwrap();
assert_eq!(log.len(), 2);
let status = repo1.status().unwrap();
assert!(
!status.entries.iter().any(|e| matches!(
e.status,
FileStatus::Untracked | FileStatus::StagedNew | FileStatus::StagedDeleted | FileStatus::Deleted
)),
"working tree should be clean after the merge: {:?}",
status.entries
);
}
#[test]
fn local_clone_reads_objects_through_source_alternates() {
let base = tempdir().unwrap();
// `other` holds a loose blob and a packed blob; `src` only refers to them
// through its alternates file.
let other = base.path().join("other");
fs::create_dir_all(&other).unwrap();
let other_repo = init_repo(&other);
let loose_alt = other_repo.write_blob(b"from alternate\n").unwrap();
let packed_alt = write_pack(&other.join(".git"), &[(ObjectKind::Blob, b"packed alternate\n".to_vec())]).unwrap()[0];
let src = base.path().join("src");
fs::create_dir_all(&src).unwrap();
let src_repo = init_repo(&src);
fs::create_dir_all(src.join(".git/objects/info")).unwrap();
fs::write(
src.join(".git/objects/info/alternates"),
format!("{}\n", other.join(".git/objects").display()),
)
.unwrap();
let own = src_repo.write_blob(b"own\n").unwrap();
let mut entries = vec![
entry("own.txt", 0o100644, own),
entry("loose.txt", 0o100644, loose_alt),
entry("packed.txt", 0o100644, packed_alt),
];
entries.sort_by(|a, b| a.name.cmp(&b.name));
let tree = src_repo.write_tree(&Tree { entries }).unwrap();
let commit = src_repo
.write_commit(&Commit {
tree,
parents: vec![],
author: sig(),
committer: sig(),
message: "uses alternates\n".into(),
})
.unwrap();
src_repo.create_branch("main", &commit).unwrap();
drop(src_repo);
let dst = base.path().join("dst");
let timings = local_clone_depth1(&src, &dst, Some("main")).expect("clone through alternates");
assert_eq!(timings.num_files, 3);
assert_eq!(fs::read_to_string(dst.join("own.txt")).unwrap(), "own\n");
assert_eq!(fs::read_to_string(dst.join("loose.txt")).unwrap(), "from alternate\n");
assert_eq!(fs::read_to_string(dst.join("packed.txt")).unwrap(), "packed alternate\n");
// The clone lists every source store as its alternate, so a repository
// opened on it reaches the alternate's objects in one hop.
let alternates = fs::read_to_string(dst.join(".git/objects/info/alternates")).unwrap();
assert_eq!(alternates.lines().count(), 2, "alternates: {}", alternates);
let mut dst_repo = Repository::open(&dst).unwrap();
assert_eq!(dst_repo.read_blob(&loose_alt).unwrap(), b"from alternate\n");
assert_eq!(dst_repo.read_blob(&packed_alt).unwrap(), b"packed alternate\n");
assert_eq!(dst_repo.head_oid().unwrap(), commit);
}