#![cfg(feature = "test-util")]
mod common;
use std::fmt::Write as _;
use std::path::Path;
use std::sync::Arc;
use bytes::Bytes;
use git_remote_object_store::object_store::ObjectStore;
use git_remote_object_store::object_store::mock::MockStore;
use git_remote_object_store::protocol::ProtocolError;
use tempfile::TempDir;
use common::{drive_in, git, git_available, git_capture, s3_url};
fn make_seed_repo() -> (TempDir, String) {
let dir = tempfile::tempdir().expect("tempdir");
git(&["init", "--quiet", "--initial-branch=main"], dir.path());
git(&["config", "user.email", "test@example.com"], dir.path());
git(&["config", "user.name", "Test"], dir.path());
git(&["config", "commit.gpgsign", "false"], dir.path());
std::fs::write(dir.path().join("hello.txt"), b"hi\n").unwrap();
git(&["add", "hello.txt"], dir.path());
git(
&["commit", "--quiet", "-m", "seed", "--no-gpg-sign"],
dir.path(),
);
let sha = git_capture(&["rev-parse", "HEAD"], dir.path());
(dir, sha.trim().to_owned())
}
fn bundle_ref(seed_dir: &Path, sha: &str, ref_name: &str) -> Bytes {
let bundles = tempfile::tempdir().expect("tempdir");
let bundle_path = bundles.path().join(format!("{sha}.bundle"));
git(
&["bundle", "create", bundle_path.to_str().unwrap(), ref_name],
seed_dir,
);
Bytes::from(std::fs::read(&bundle_path).expect("read bundle"))
}
fn make_dst_repo() -> TempDir {
let dir = tempfile::tempdir().expect("tempdir");
git(&["init", "--quiet"], dir.path());
dir
}
#[tokio::test]
async fn idle_blank_line_with_fetch_wiring_emits_terminator() {
let dst = make_dst_repo();
let (out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"\n",
dst.path().to_path_buf(),
)
.await;
result.expect("blank line should succeed");
assert_eq!(&out, b"\n");
}
#[tokio::test]
async fn single_fetch_downloads_and_unbundles_into_local_repo() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, sha) = make_seed_repo();
let bundle = bundle_ref(seed.path(), &sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{sha}.bundle"), bundle);
let dst = make_dst_repo();
let script = format!("fetch {sha} refs/heads/main\n\n");
let (out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("fetch should succeed");
assert_eq!(&out, b"\n", "fetch is silent except for terminator");
let dst_sha = git_capture(&["rev-parse", &sha], dst.path());
assert_eq!(dst_sha.trim(), sha);
}
#[tokio::test]
async fn fetch_works_with_no_prefix() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, sha) = make_seed_repo();
let bundle = bundle_ref(seed.path(), &sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("refs/heads/main/{sha}.bundle"), bundle);
let dst = make_dst_repo();
let script = format!("fetch {sha} refs/heads/main\n\n");
let (out, result) = drive_in(
s3_url(None),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("fetch should succeed");
assert_eq!(&out, b"\n");
let dst_sha = git_capture(&["rev-parse", &sha], dst.path());
assert_eq!(dst_sha.trim(), sha);
}
#[tokio::test]
async fn multiple_fetches_run_to_completion() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
git(&["init", "--quiet", "--initial-branch=main"], seed.path());
git(&["config", "user.email", "test@example.com"], seed.path());
git(&["config", "user.name", "Test"], seed.path());
git(&["config", "commit.gpgsign", "false"], seed.path());
let mut shas = Vec::new();
for i in 0..3 {
std::fs::write(seed.path().join(format!("f{i}.txt")), b"x\n").unwrap();
git(&["add", "."], seed.path());
git(
&["commit", "--quiet", "-m", "step", "--no-gpg-sign"],
seed.path(),
);
let sha = git_capture(&["rev-parse", "HEAD"], seed.path())
.trim()
.to_owned();
let ref_name = format!("refs/heads/branch-{i}");
git(&["update-ref", &ref_name, &sha], seed.path());
shas.push((sha, ref_name));
}
let store = MockStore::new();
for (sha, ref_name) in &shas {
let bundle = bundle_ref(seed.path(), sha, ref_name);
store.insert(format!("repo/{ref_name}/{sha}.bundle"), bundle);
}
let dst = make_dst_repo();
let mut script = String::new();
for (sha, ref_name) in &shas {
writeln!(script, "fetch {sha} {ref_name}").unwrap();
}
script.push('\n');
let (out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("multi fetch should succeed");
assert_eq!(&out, b"\n");
for (sha, _) in &shas {
let dst_sha = git_capture(&["rev-parse", sha], dst.path());
assert_eq!(dst_sha.trim(), *sha, "all fetched commits must resolve");
}
}
#[tokio::test]
async fn duplicate_shas_in_batch_are_handled_safely() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, sha) = make_seed_repo();
let bundle = bundle_ref(seed.path(), &sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{sha}.bundle"), bundle);
let dst = make_dst_repo();
let line = format!("fetch {sha} refs/heads/main\n");
let script = format!("{}\n", line.repeat(20));
let (out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("duplicate-SHA batch should succeed");
assert_eq!(&out, b"\n");
let dst_sha = git_capture(&["rev-parse", &sha], dst.path());
assert_eq!(dst_sha.trim(), sha);
}
#[tokio::test]
async fn fetch_missing_bundle_propagates_error() {
let dst = make_dst_repo();
let sha = "0123456789abcdef0123456789abcdef01234567";
let script = format!("fetch {sha} refs/heads/main\n\n");
let (out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
&script,
dst.path().to_path_buf(),
)
.await;
match result {
Err(ProtocolError::Fetch(_)) => {}
other => panic!("expected Fetch error, got {other:?}"),
}
assert!(out.is_empty(), "fetch must not write on error: {out:?}");
}
#[tokio::test]
async fn fetch_invalid_sha_returns_error() {
use git_remote_object_store::protocol::fetch::FetchError;
let dst = make_dst_repo();
let script = "fetch notahex refs/heads/main\n\n";
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
script,
dst.path().to_path_buf(),
)
.await;
match result {
Err(ProtocolError::Fetch(FetchError::Sha(_))) => {}
other => panic!("expected Fetch(Sha) error, got {other:?}"),
}
}
#[tokio::test]
async fn fetched_refs_dedupes_across_batches() {
use git_remote_object_store::protocol::run;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, sha) = make_seed_repo();
let bundle = bundle_ref(seed.path(), &sha, "refs/heads/main");
let store = Arc::new(MockStore::new());
let key = format!("repo/refs/heads/main/{sha}.bundle");
store.insert(&key, bundle);
let dst = make_dst_repo();
let remote = s3_url(Some("repo"));
let dst_path = dst.path().to_path_buf();
let (client_side, helper_side) = tokio::io::duplex(64 * 1024);
let (helper_in, helper_out) = tokio::io::split(helper_side);
let (mut client_reader, mut client_writer) = tokio::io::split(client_side);
let store_for_run: Arc<dyn ObjectStore> = Arc::clone(&store) as _;
let run_task = tokio::spawn(async move {
run(
remote,
store_for_run,
git_remote_object_store::url::StorageEngine::Bundle,
BufReader::new(helper_in),
helper_out,
None,
dst_path,
)
.await
});
client_writer
.write_all(format!("fetch {sha} refs/heads/main\n\n").as_bytes())
.await
.unwrap();
let mut buf = [0u8; 1];
client_reader.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"\n", "batch 1 should emit the terminator");
store
.delete(&key)
.await
.expect("bundle must be present from setup");
client_writer
.write_all(format!("fetch {sha} refs/heads/main\n\n").as_bytes())
.await
.unwrap();
match tokio::time::timeout(
std::time::Duration::from_secs(5),
client_reader.read_exact(&mut buf),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(read_err)) => {
client_writer.shutdown().await.ok();
let run_outcome = run_task.await;
panic!(
"batch 2 emitted no terminator (read error: {read_err}); run() outcome: \
{run_outcome:?} — dedup likely broken: helper attempted a forbidden re-fetch \
of the deleted bundle"
);
}
Err(elapsed) => {
panic!("batch 2 read timed out after {elapsed} — helper appears stuck")
}
}
assert_eq!(&buf, b"\n", "batch 2 should emit the terminator");
client_writer.shutdown().await.unwrap();
let result = run_task.await.unwrap();
result
.expect("second batch must short-circuit via fetched_refs even though the bundle is gone");
}
#[tokio::test]
async fn fetch_with_depth_writes_shallow_file_with_boundary() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
git(&["init", "--quiet", "--initial-branch=main"], seed.path());
git(&["config", "user.email", "test@example.com"], seed.path());
git(&["config", "user.name", "Test"], seed.path());
git(&["config", "commit.gpgsign", "false"], seed.path());
for i in 0..3u32 {
std::fs::write(seed.path().join("hello.txt"), format!("hi {i}\n")).unwrap();
git(&["add", "hello.txt"], seed.path());
git(
&["commit", "--quiet", "-m", &format!("c{i}"), "--no-gpg-sign"],
seed.path(),
);
}
let tip_sha = git_capture(&["rev-parse", "HEAD"], seed.path())
.trim()
.to_owned();
let bundle = bundle_ref(seed.path(), &tip_sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{tip_sha}.bundle"), bundle);
let dst = make_dst_repo();
let script = format!("option depth 1\nfetch {tip_sha} refs/heads/main\n\n");
let (out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("fetch with depth should succeed");
assert_eq!(&out, b"ok\n\n", "expected option ack + terminator");
let shallow_path = dst.path().join(".git").join("shallow");
let shallow = std::fs::read_to_string(&shallow_path).expect("shallow file should exist");
assert_eq!(
shallow.trim(),
tip_sha,
"shallow boundary should be HEAD (tip appears parentless); got {shallow:?}"
);
}
#[tokio::test]
async fn fetch_without_depth_does_not_write_shallow_file() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, sha) = make_seed_repo();
let bundle = bundle_ref(seed.path(), &sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{sha}.bundle"), bundle);
let dst = make_dst_repo();
let script = format!("fetch {sha} refs/heads/main\n\n");
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("fetch should succeed");
let shallow_path = dst.path().join(".git").join("shallow");
assert!(
!shallow_path.exists(),
".git/shallow unexpectedly created without `option depth`"
);
}
#[tokio::test]
async fn fetch_with_depth_exceeding_history_does_not_write_shallow_file() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, sha) = make_seed_repo(); let bundle = bundle_ref(seed.path(), &sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{sha}.bundle"), bundle);
let dst = make_dst_repo();
let script = format!("option depth 10\nfetch {sha} refs/heads/main\n\n");
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("fetch with depth exceeding history should succeed");
let shallow_path = dst.path().join(".git").join("shallow");
assert!(
!shallow_path.exists(),
".git/shallow must not be created when depth exceeds total history length"
);
}
#[tokio::test]
async fn depth_resets_between_batches() {
use git_remote_object_store::protocol::run;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
git(&["init", "--quiet", "--initial-branch=main"], seed.path());
git(&["config", "user.email", "test@example.com"], seed.path());
git(&["config", "user.name", "Test"], seed.path());
git(&["config", "commit.gpgsign", "false"], seed.path());
std::fs::write(seed.path().join("a.txt"), b"a\n").unwrap();
git(&["add", "a.txt"], seed.path());
git(
&["commit", "--quiet", "-m", "c0", "--no-gpg-sign"],
seed.path(),
);
std::fs::write(seed.path().join("a.txt"), b"a2\n").unwrap();
git(&["add", "a.txt"], seed.path());
git(
&["commit", "--quiet", "-m", "c1", "--no-gpg-sign"],
seed.path(),
);
let tip_sha = git_capture(&["rev-parse", "HEAD"], seed.path())
.trim()
.to_owned();
let bundle_main = bundle_ref(seed.path(), &tip_sha, "refs/heads/main");
git(&["checkout", "--orphan", "side"], seed.path());
git(&["rm", "-rf", "."], seed.path());
std::fs::write(seed.path().join("b.txt"), b"b\n").unwrap();
git(&["add", "b.txt"], seed.path());
git(
&["commit", "--quiet", "-m", "side0", "--no-gpg-sign"],
seed.path(),
);
std::fs::write(seed.path().join("b.txt"), b"b2\n").unwrap();
git(&["add", "b.txt"], seed.path());
git(
&["commit", "--quiet", "-m", "side1", "--no-gpg-sign"],
seed.path(),
);
let side_sha = git_capture(&["rev-parse", "HEAD"], seed.path())
.trim()
.to_owned();
let bundle_side = bundle_ref(seed.path(), &side_sha, "refs/heads/side");
let store = Arc::new(MockStore::new());
store.insert(
format!("repo/refs/heads/main/{tip_sha}.bundle"),
bundle_main,
);
store.insert(
format!("repo/refs/heads/side/{side_sha}.bundle"),
bundle_side,
);
let dst = make_dst_repo();
let dst_path = dst.path().to_path_buf();
let remote = s3_url(Some("repo"));
let (client_side, helper_side) = tokio::io::duplex(64 * 1024);
let (helper_in, helper_out) = tokio::io::split(helper_side);
let (mut client_reader, mut client_writer) = tokio::io::split(client_side);
let store_for_run: Arc<dyn ObjectStore> = Arc::clone(&store) as _;
let run_task = tokio::spawn(async move {
run(
remote,
store_for_run,
git_remote_object_store::url::StorageEngine::Bundle,
BufReader::new(helper_in),
helper_out,
None,
dst_path,
)
.await
});
client_writer
.write_all(format!("option depth 1\nfetch {tip_sha} refs/heads/main\n\n").as_bytes())
.await
.unwrap();
let mut buf = [0u8; 4];
client_reader.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ok\n\n");
client_writer
.write_all(format!("fetch {side_sha} refs/heads/side\n\n").as_bytes())
.await
.unwrap();
let mut term = [0u8; 1];
client_reader.read_exact(&mut term).await.unwrap();
assert_eq!(&term, b"\n");
client_writer.shutdown().await.unwrap();
let _ = run_task.await.unwrap();
let shallow_path = dst.path().join(".git").join("shallow");
let shallow = std::fs::read_to_string(&shallow_path).expect("shallow file should exist");
assert_eq!(
shallow.trim(),
tip_sha,
"shallow file polluted by depth-less batch 2: {shallow:?}",
);
}
#[tokio::test]
async fn shallow_fetch_limits_visible_commit_count() {
const TOTAL_COMMITS: u32 = 5;
const DEPTH: u32 = 3;
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
git(&["init", "--quiet", "--initial-branch=main"], seed.path());
git(&["config", "user.email", "test@example.com"], seed.path());
git(&["config", "user.name", "Test"], seed.path());
git(&["config", "commit.gpgsign", "false"], seed.path());
for i in 0..TOTAL_COMMITS {
std::fs::write(seed.path().join("f.txt"), format!("{i}\n")).unwrap();
git(&["add", "f.txt"], seed.path());
git(
&["commit", "--quiet", "-m", &format!("c{i}"), "--no-gpg-sign"],
seed.path(),
);
}
let tip_sha = git_capture(&["rev-parse", "HEAD"], seed.path())
.trim()
.to_owned();
let bundle = bundle_ref(seed.path(), &tip_sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{tip_sha}.bundle"), bundle);
let dst = make_dst_repo();
let script = format!("option depth {DEPTH}\nfetch {tip_sha} refs/heads/main\n\n");
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("shallow fetch should succeed");
git(&["update-ref", "refs/heads/main", &tip_sha], dst.path());
let log = git_capture(&["log", "--oneline", "refs/heads/main"], dst.path());
let visible = log.lines().count();
assert_eq!(
visible, DEPTH as usize,
"expected {DEPTH} visible commits after depth={DEPTH} fetch, got {visible}: {log:?}"
);
}
#[tokio::test]
async fn bundle_fetch_round_trip_of_annotated_tag_resolves_tag_object() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let (seed, _commit_sha, tag_sha) = common::make_seed_repo_with_annotated_tag("primary", "v1");
let store = Arc::new(MockStore::new());
let (_, push_result) = drive_in(
s3_url(Some("repo")),
Arc::clone(&store) as Arc<dyn ObjectStore>,
"push refs/tags/v1:refs/tags/v1\n\n",
seed.path().to_path_buf(),
)
.await;
push_result.expect("bundle annotated-tag push must succeed");
let dst = tempfile::tempdir().expect("dst tempdir");
git(&["init", "--quiet", "--initial-branch=main"], dst.path());
git(&["config", "user.email", "test@example.com"], dst.path());
git(&["config", "user.name", "Test"], dst.path());
git(&["config", "commit.gpgsign", "false"], dst.path());
let fetch_script = format!("fetch {tag_sha} refs/tags/v1\n\n");
let (_, fetch_result) = drive_in(
s3_url(Some("repo")),
Arc::clone(&store) as Arc<dyn ObjectStore>,
&fetch_script,
dst.path().to_path_buf(),
)
.await;
fetch_result.expect("bundle fetch of tag must succeed");
let kind = std::process::Command::new("git")
.args(["cat-file", "-t", &tag_sha])
.current_dir(dst.path())
.output()
.expect("spawn git cat-file");
assert!(
kind.status.success(),
"git cat-file -t failed: {}",
String::from_utf8_lossy(&kind.stderr),
);
assert_eq!(
String::from_utf8(kind.stdout).unwrap().trim(),
"tag",
"tag OID must decode as a tag object after bundle fetch round-trip",
);
}
fn build_linear_history(dir: &Path, n: usize) -> Vec<String> {
git(&["init", "--quiet", "--initial-branch=main"], dir);
git(&["config", "user.email", "test@example.com"], dir);
git(&["config", "user.name", "Test"], dir);
git(&["config", "commit.gpgsign", "false"], dir);
let mut shas = Vec::with_capacity(n);
for i in 0..n {
std::fs::write(dir.join("hello.txt"), format!("hi {i}\n")).unwrap();
git(&["add", "hello.txt"], dir);
git(
&["commit", "--quiet", "-m", &format!("c{i}"), "--no-gpg-sign"],
dir,
);
let sha = git_capture(&["rev-parse", "HEAD"], dir).trim().to_owned();
shas.push(sha);
}
shas
}
#[tokio::test]
async fn fetch_with_depth_3_after_depth_1_deepens() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
let shas = build_linear_history(seed.path(), 5);
let tip_sha = &shas[4];
let depth_3_boundary = &shas[2];
let bundle = bundle_ref(seed.path(), tip_sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{tip_sha}.bundle"), bundle);
let dst = make_dst_repo();
let shallow_path = dst.path().join(".git").join("shallow");
std::fs::write(&shallow_path, format!("{tip_sha}\n")).unwrap();
let script = format!("option depth 3\nfetch {tip_sha} refs/heads/main\n\n");
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("deepening fetch should succeed");
let shallow = std::fs::read_to_string(&shallow_path).expect("shallow exists");
assert_eq!(
shallow.trim(),
depth_3_boundary,
"after deepen-from-1-to-3, shallow file must contain exactly the depth-3 boundary; got {shallow:?}"
);
}
#[tokio::test]
async fn fetch_with_depth_unlinks_stale_shallow_when_history_fits() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
let shas = build_linear_history(seed.path(), 3);
let tip_sha = &shas[2];
let bundle = bundle_ref(seed.path(), tip_sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{tip_sha}.bundle"), bundle);
let dst = make_dst_repo();
let shallow_path = dst.path().join(".git").join("shallow");
std::fs::write(&shallow_path, format!("{tip_sha}\n")).unwrap();
let script = format!("option depth 10\nfetch {tip_sha} refs/heads/main\n\n");
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("deepen-to-full fetch should succeed");
assert!(
!shallow_path.exists(),
"shallow file must be unlinked when no boundaries remain after pruning"
);
}
#[tokio::test]
async fn fetch_re_shallow_to_smaller_depth() {
if !git_available() {
eprintln!("skipping: git not on PATH");
return;
}
let seed = tempfile::tempdir().expect("tempdir");
let shas = build_linear_history(seed.path(), 5);
let tip_sha = &shas[4];
let depth_3_boundary = &shas[2];
let bundle = bundle_ref(seed.path(), tip_sha, "refs/heads/main");
let store = MockStore::new();
store.insert(format!("repo/refs/heads/main/{tip_sha}.bundle"), bundle);
let dst = make_dst_repo();
let shallow_path = dst.path().join(".git").join("shallow");
std::fs::write(&shallow_path, format!("{depth_3_boundary}\n")).unwrap();
let script = format!("option depth 1\nfetch {tip_sha} refs/heads/main\n\n");
let (_out, result) = drive_in(
s3_url(Some("repo")),
Arc::new(store),
&script,
dst.path().to_path_buf(),
)
.await;
result.expect("re-shallow fetch should succeed");
let shallow = std::fs::read_to_string(&shallow_path).expect("shallow exists");
assert_eq!(
shallow.trim(),
tip_sha,
"re-shallow to depth-1 must replace [T-2] with [tip]; got {shallow:?}"
);
}