#![cfg(feature = "http-ureq")]
#![cfg(unix)]
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use grit_lib::error::Result as GritResult;
use grit_lib::fetch::{fetch_remote, NoProgress};
use grit_lib::objects::ObjectId;
use grit_lib::odb::Odb;
use grit_lib::refs::resolve_ref;
use grit_lib::transfer::{FetchOptions, FetchOutcome, TagMode, UpdateMode};
use grit_lib::transport::http::ureq_client::UreqHttpClient;
use grit_lib::transport::http::http_fetch;
use grit_lib::transport::{ConnectOptions, GitDaemonTransport, Service, SshTransport, Transport};
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "T")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
.env("GIT_COMMITTER_NAME", "T")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("run git");
assert!(
out.status.success(),
"git {args:?} in {} failed (status {:?}): stderr={} stdout={}",
dir.display(),
out.status.code(),
String::from_utf8_lossy(&out.stderr),
String::from_utf8_lossy(&out.stdout)
);
String::from_utf8(out.stdout).expect("utf8 git output")
}
fn rev_parse(dir: &Path, rev: &str) -> ObjectId {
ObjectId::from_hex(git(dir, &["rev-parse", rev]).trim()).expect("valid oid")
}
fn open_odb(git_dir: &Path) -> Odb {
Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf())
}
fn fsck_clean(local_root: &Path) {
let fsck = Command::new("git")
.current_dir(local_root)
.args(["fsck", "--no-dangling"])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("run git fsck");
assert!(
fsck.status.success(),
"git fsck failed: {}\n{}",
String::from_utf8_lossy(&fsck.stdout),
String::from_utf8_lossy(&fsck.stderr)
);
}
fn init_local(root: &Path) -> PathBuf {
std::fs::create_dir_all(root).unwrap();
git(root, &["init", "-q", "-b", "main", "."]);
root.join(".git")
}
fn build_source_work(dir: &Path) {
git(dir, &["init", "-q", "-b", "main", "."]);
std::fs::write(dir.join("a.txt"), "one\n").unwrap();
git(dir, &["add", "a.txt"]);
git(dir, &["commit", "-q", "-m", "c1"]);
std::fs::write(dir.join("b.txt"), "two\n").unwrap();
git(dir, &["add", "b.txt"]);
git(dir, &["commit", "-q", "-m", "c2"]);
git(dir, &["tag", "-a", "v1", "-m", "release one"]);
git(dir, &["branch", "topic"]);
git(dir, &["checkout", "-q", "-b", "side"]);
git(dir, &["commit", "-q", "--allow-empty", "-m", "side1"]);
git(dir, &["tag", "-a", "vside", "-m", "side tag"]);
git(dir, &["checkout", "-q", "main"]);
}
fn bare_clone(work: &Path, bare: &Path) {
git(
work,
&["clone", "-q", "--bare", ".", bare.to_str().expect("utf8")],
);
git(bare, &["symbolic-ref", "HEAD", "refs/heads/main"]);
}
fn free_port() -> Option<u16> {
static USED: std::sync::Mutex<Vec<u16>> = std::sync::Mutex::new(Vec::new());
let mut used = USED.lock().unwrap_or_else(|e| e.into_inner());
for _ in 0..200 {
let l = TcpListener::bind(("127.0.0.1", 0)).ok()?;
let p = l.local_addr().ok()?.port();
drop(l);
if !used.contains(&p) {
used.push(p);
return Some(p);
}
}
None
}
fn wait_ready(port: u16) -> bool {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok() {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
false
}
struct ServerHandle {
_proc: Option<Child>,
base: PathBuf,
}
impl Drop for ServerHandle {
fn drop(&mut self) {
if let Some(child) = self._proc.as_mut() {
let _ = child.kill();
let _ = child.wait();
}
}
}
fn find_binary(name: &str) -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let deps = exe.parent()?; let profile = deps.parent()?; for cand in [profile.join(name), deps.join(name)] {
if cand.is_file() {
return Some(cand);
}
}
None
}
fn write_fake_ssh(dir: &Path) -> Option<PathBuf> {
use std::os::unix::fs::PermissionsExt;
let script = dir.join("fake-ssh.sh");
let body = r#"#!/bin/sh
cmd=
for cmd in "$@"; do :; done
case "$cmd" in
"git-upload-pack "*) cmd="git upload-pack ${cmd#git-upload-pack }" ;;
"git-receive-pack "*) cmd="git receive-pack ${cmd#git-receive-pack }" ;;
esac
eval "exec $cmd"
"#;
std::fs::write(&script, body).ok()?;
let mut perms = std::fs::metadata(&script).ok()?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).ok()?;
Some(script)
}
struct Driver {
name: &'static str,
#[allow(dead_code)]
protocol: u8,
serve: Box<dyn Fn(&Path) -> Option<(ServerHandle, String)>>,
fetch: Box<dyn Fn(&str, &Path, &FetchOptions) -> GritResult<FetchOutcome>>,
}
fn daemon_driver(protocol: u8) -> Driver {
let label = if protocol >= 2 {
"git-daemon/v2"
} else {
"git-daemon/v1"
};
Driver {
name: label,
protocol,
serve: Box::new(|source: &Path| {
let base = source.parent()?.join(format!(
"daemon-base-{}",
source.file_name()?.to_string_lossy()
));
std::fs::create_dir_all(&base).ok()?;
let served = base.join("repo.git");
let st = Command::new("git")
.args([
"clone",
"-q",
"--bare",
source.to_str()?,
served.to_str()?,
])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.status()
.ok()?;
if !st.success() {
return None;
}
let _ = Command::new("git")
.current_dir(&served)
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
.status();
let port = free_port()?;
let child = Command::new("git")
.arg("daemon")
.arg("--listen=127.0.0.1")
.arg(format!("--port={port}"))
.arg("--reuseaddr")
.arg("--export-all")
.arg(format!("--base-path={}", base.display()))
.arg(&base)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()?;
if !wait_ready(port) {
let mut c = child;
let _ = c.kill();
let _ = c.wait();
return None;
}
let url = format!("git://127.0.0.1:{port}/repo.git");
Some((
ServerHandle {
_proc: Some(child),
base,
},
url,
))
}),
fetch: Box::new(move |url, local_git, opts| {
let transport = GitDaemonTransport::new();
let copts = ConnectOptions {
protocol_version: protocol,
..Default::default()
};
let mut conn = transport.connect(url, Service::UploadPack, &copts)?;
fetch_remote(local_git, &mut *conn, opts, &mut NoProgress)
}),
}
}
fn ssh_driver(protocol: u8, scratch: PathBuf) -> Option<Driver> {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
return None;
}
let fake_ssh = write_fake_ssh(&scratch)?;
let label = if protocol >= 2 { "ssh/v2" } else { "ssh/v1" };
Some(Driver {
name: label,
protocol,
serve: Box::new(|source: &Path| {
let url = format!("ssh://git@fakehost{}", source.to_str()?);
Some((
ServerHandle {
_proc: None,
base: source.to_path_buf(),
},
url,
))
}),
fetch: Box::new(move |url, local_git, opts| {
let transport = SshTransport::with_program(fake_ssh.as_os_str());
let copts = ConnectOptions {
protocol_version: protocol,
..Default::default()
};
let mut conn = transport.connect(url, Service::UploadPack, &copts)?;
fetch_remote(local_git, &mut *conn, opts, &mut NoProgress)
}),
})
}
fn http_driver(protocol: u8) -> Option<Driver> {
let grit_bin = find_binary("grit")?;
let server_bin = find_binary("grit-http-server")?;
let label = if protocol >= 2 { "http/v2" } else { "http/v1" };
Some(Driver {
name: label,
protocol,
serve: Box::new(move |source: &Path| {
let root = source.parent()?.join(format!(
"http-root-{}",
source.file_name()?.to_string_lossy()
));
std::fs::create_dir_all(&root).ok()?;
let served = root.join("repo.git");
let st = Command::new("git")
.args([
"clone",
"-q",
"--bare",
source.to_str()?,
served.to_str()?,
])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.status()
.ok()?;
if !st.success() {
return None;
}
let _ = Command::new("git")
.current_dir(&served)
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
.status();
let port = free_port()?;
let child = Command::new(&server_bin)
.arg("--root")
.arg(&root)
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.env("GUST_BIN", &grit_bin)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()?;
if !wait_ready(port) {
let mut c = child;
let _ = c.kill();
let _ = c.wait();
return None;
}
let url = format!("http://127.0.0.1:{port}/repo.git");
Some((
ServerHandle {
_proc: Some(child),
base: root,
},
url,
))
}),
fetch: Box::new(move |url, local_git, opts| {
let client = if protocol >= 2 {
UreqHttpClient::new().with_git_protocol("version=2")
} else {
UreqHttpClient::new()
};
http_fetch(&client, local_git, url, opts, &mut NoProgress)
}),
})
}
fn drivers(scratch: &Path) -> Vec<Driver> {
let mut v: Vec<Driver> = Vec::new();
v.push(daemon_driver(0));
v.push(daemon_driver(2));
if let Some(d) = ssh_driver(0, scratch.to_path_buf()) {
v.push(d);
}
if let Some(d) = ssh_driver(2, scratch.to_path_buf()) {
v.push(d);
}
if let Some(d) = http_driver(0) {
v.push(d);
}
if let Some(d) = http_driver(2) {
v.push(d);
}
v
}
struct Harness {
tmp: tempfile::TempDir,
}
impl Harness {
fn new() -> Self {
Harness {
tmp: tempfile::tempdir().expect("tempdir"),
}
}
fn path(&self) -> &Path {
self.tmp.path()
}
fn make_source(&self, name: &str, build: impl FnOnce(&Path)) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let uniq = SEQ.fetch_add(1, Ordering::SeqCst);
let work = self.path().join(format!("{name}-{uniq}-work"));
std::fs::create_dir_all(&work).unwrap();
build(&work);
let bare = self.path().join(format!("{name}-{uniq}.git"));
bare_clone(&work, &bare);
bare
}
fn drivers(&self) -> Vec<Driver> {
drivers(self.path())
}
}
fn for_each_driver(
h: &Harness,
build_source: &dyn Fn(&Path),
body: &dyn Fn(&Driver, &dyn Fn(&Path, &FetchOptions) -> GritResult<FetchOutcome>, &Path, &Path),
) -> usize {
let mut ran = 0usize;
for (i, driver) in h.drivers().into_iter().enumerate() {
let src = h.make_source(&format!("src-{}-{i}", driver.name.replace('/', "-")), build_source);
let Some((handle, url)) = (driver.serve)(&src) else {
eprintln!("SKIP driver {}: server bring-up failed", driver.name);
continue;
};
let served = if handle._proc.is_some() {
handle.base.join("repo.git")
} else {
src.clone()
};
let src_tag = src
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("src")
.trim_end_matches(".git");
let local_root = h.path().join(format!("local-{src_tag}"));
let local_git = init_local(&local_root);
let fetch = |opts_local_git: &Path, opts: &FetchOptions| {
(driver.fetch)(&url, opts_local_git, opts)
};
let bound = |lg: &Path, opts: &FetchOptions| fetch(lg, opts);
body(&driver, &bound, &served, &local_git);
ran += 1;
drop(handle);
}
ran
}
#[test]
fn matrix_wildcard_refspec_all_heads() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let main_oid = rev_parse(served, "refs/heads/main");
let topic_oid = rev_parse(served, "refs/heads/topic");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
let outcome = fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] wildcard fetch failed: {e}", driver.name));
let got_main = resolve_ref(local_git, "refs/remotes/origin/main")
.unwrap_or_else(|_| panic!("[{}] origin/main missing", driver.name));
let got_topic = resolve_ref(local_git, "refs/remotes/origin/topic")
.unwrap_or_else(|_| panic!("[{}] origin/topic missing", driver.name));
assert_eq!(got_main, main_oid, "[{}] origin/main oid", driver.name);
assert_eq!(got_topic, topic_oid, "[{}] origin/topic oid", driver.name);
let side_oid = rev_parse(served, "refs/heads/side");
let got_side = resolve_ref(local_git, "refs/remotes/origin/side")
.unwrap_or_else(|_| panic!("[{}] origin/side missing", driver.name));
assert_eq!(got_side, side_oid, "[{}] origin/side oid", driver.name);
assert_eq!(
outcome.default_branch.as_deref(),
Some("main"),
"[{}] default_branch from HEAD symref",
driver.name
);
let mu = outcome
.updates
.iter()
.find(|u| u.remote_ref == "refs/heads/main")
.unwrap_or_else(|| panic!("[{}] no update for main", driver.name));
assert_eq!(mu.mode, UpdateMode::New, "[{}] main mode", driver.name);
assert_eq!(mu.new_oid, Some(main_oid));
let odb = open_odb(local_git);
for oid in [main_oid, topic_oid, side_oid] {
assert!(
odb.exists(&oid),
"[{}] object {} missing",
driver.name,
oid.to_hex()
);
odb.read(&oid)
.unwrap_or_else(|e| panic!("[{}] read {}: {e}", driver.name, oid.to_hex()));
}
assert_eq!(
got_main.to_hex(),
git(served, &["rev-parse", "refs/heads/main"]).trim(),
"[{}] main tip vs git rev-parse",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for the wildcard test");
}
#[test]
fn matrix_exact_refspec_single_head() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let main_oid = rev_parse(served, "refs/heads/main");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/main:refs/remotes/origin/main".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] exact fetch failed: {e}", driver.name));
assert_eq!(
resolve_ref(local_git, "refs/remotes/origin/main")
.unwrap_or_else(|_| panic!("[{}] origin/main missing", driver.name)),
main_oid,
"[{}] exact main oid",
driver.name
);
assert!(
resolve_ref(local_git, "refs/remotes/origin/topic").is_err(),
"[{}] exact refspec must NOT write origin/topic",
driver.name
);
assert!(
resolve_ref(local_git, "refs/remotes/origin/side").is_err(),
"[{}] exact refspec must NOT write origin/side",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for the exact-refspec test");
}
#[test]
fn matrix_negative_refspec_excludes_head() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let main_oid = rev_parse(served, "refs/heads/main");
let topic_oid = rev_parse(served, "refs/heads/topic");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
negative_refspecs: vec!["^refs/heads/topic".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] negative fetch failed: {e}", driver.name));
assert_eq!(
resolve_ref(local_git, "refs/remotes/origin/main")
.unwrap_or_else(|_| panic!("[{}] origin/main missing", driver.name)),
main_oid,
"[{}] main kept",
driver.name
);
assert!(
resolve_ref(local_git, "refs/remotes/origin/topic").is_err(),
"[{}] negative refspec must exclude origin/topic (got {})",
driver.name,
topic_oid.to_hex()
);
assert!(
resolve_ref(local_git, "refs/remotes/origin/side").is_ok(),
"[{}] non-excluded head origin/side must remain",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for the negative-refspec test");
}
#[test]
fn matrix_tag_modes_all_following_none() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let v1_oid = rev_parse(served, "refs/tags/v1");
let vside_oid = rev_parse(served, "refs/tags/vside");
{
let opts = FetchOptions {
refspecs: vec!["+refs/heads/main:refs/remotes/origin/main".to_owned()],
tags: TagMode::All,
..Default::default()
};
fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] tags=All fetch failed: {e}", driver.name));
assert_eq!(
resolve_ref(local_git, "refs/tags/v1")
.unwrap_or_else(|_| panic!("[{}] v1 (All) missing", driver.name)),
v1_oid,
"[{}] tag v1 (All)",
driver.name
);
assert_eq!(
resolve_ref(local_git, "refs/tags/vside").unwrap_or_else(|_| panic!(
"[{}] vside (All) missing: TagMode::All must fetch unreachable tags",
driver.name
)),
vside_oid,
"[{}] tag vside (All)",
driver.name
);
fsck_clean(local_git.parent().unwrap());
}
},
);
assert!(ran > 0, "no transport driver was available for tags=All");
let ran2 = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let v1_oid = rev_parse(served, "refs/tags/v1");
let opts = FetchOptions {
refspecs: vec!["+refs/heads/main:refs/remotes/origin/main".to_owned()],
tags: TagMode::Following,
..Default::default()
};
fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] tags=Following fetch failed: {e}", driver.name));
assert_eq!(
resolve_ref(local_git, "refs/tags/v1")
.unwrap_or_else(|_| panic!("[{}] v1 (Following) missing", driver.name)),
v1_oid,
"[{}] Following keeps reachable v1",
driver.name
);
assert!(
resolve_ref(local_git, "refs/tags/vside").is_err(),
"[{}] Following must drop the unreachable vside tag",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran2 > 0, "no transport driver was available for tags=Following");
let ran3 = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, _served, local_git| {
let opts = FetchOptions {
refspecs: vec!["+refs/heads/main:refs/remotes/origin/main".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] tags=None fetch failed: {e}", driver.name));
assert!(
resolve_ref(local_git, "refs/tags/v1").is_err(),
"[{}] tags=None must NOT write v1",
driver.name
);
assert!(
resolve_ref(local_git, "refs/tags/vside").is_err(),
"[{}] tags=None must NOT write vside",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran3 > 0, "no transport driver was available for tags=None");
}
#[test]
fn matrix_prune_vanished_wildcard_ref() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let wildcard = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &wildcard)
.unwrap_or_else(|e| panic!("[{}] initial wildcard fetch failed: {e}", driver.name));
assert!(
resolve_ref(local_git, "refs/remotes/origin/topic").is_ok(),
"[{}] origin/topic must exist before prune",
driver.name
);
git(served, &["branch", "-D", "topic"]);
let pruning = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
prune: true,
tags: TagMode::None,
..Default::default()
};
let outcome = fetch(local_git, &pruning)
.unwrap_or_else(|e| panic!("[{}] pruning fetch failed: {e}", driver.name));
assert!(
resolve_ref(local_git, "refs/remotes/origin/topic").is_err(),
"[{}] origin/topic must be pruned after it vanished on the remote",
driver.name
);
assert!(
resolve_ref(local_git, "refs/remotes/origin/main").is_ok(),
"[{}] origin/main must survive the prune",
driver.name
);
assert!(
outcome.updates.iter().any(|u| u.mode == UpdateMode::DeletedMissing
&& u.local_ref.as_deref() == Some("refs/remotes/origin/topic")),
"[{}] prune must report a DeletedMissing for origin/topic; got {:?}",
driver.name,
outcome.updates
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for wildcard prune");
}
#[test]
fn matrix_prune_vanished_exact_ref() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let seed = FetchOptions {
refspecs: vec!["+refs/heads/topic:refs/remotes/origin/topic".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &seed)
.unwrap_or_else(|e| panic!("[{}] seed exact fetch failed: {e}", driver.name));
assert!(
resolve_ref(local_git, "refs/remotes/origin/topic").is_ok(),
"[{}] origin/topic must exist before exact prune",
driver.name
);
git(served, &["branch", "-D", "topic"]);
let prune_exact = FetchOptions {
refspecs: vec!["+refs/heads/topic:refs/remotes/origin/topic".to_owned()],
prune: true,
tags: TagMode::None,
..Default::default()
};
let outcome = fetch(local_git, &prune_exact).unwrap_or_else(|e| {
panic!("[{}] exact pruning fetch failed: {e}", driver.name)
});
assert!(
resolve_ref(local_git, "refs/remotes/origin/topic").is_err(),
"[{}] exact-refspec prune must remove origin/topic",
driver.name
);
assert!(
outcome.updates.iter().any(|u| u.mode == UpdateMode::DeletedMissing
&& u.local_ref.as_deref() == Some("refs/remotes/origin/topic")),
"[{}] exact prune must report DeletedMissing for origin/topic; got {:?}",
driver.name,
outcome.updates
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for exact prune");
}
#[test]
fn matrix_prune_before_update_df_conflict() {
let h = Harness::new();
let build = |w: &Path| {
git(w, &["init", "-q", "-b", "main", "."]);
std::fs::write(w.join("a.txt"), "one\n").unwrap();
git(w, &["add", "a.txt"]);
git(w, &["commit", "-q", "-m", "c1"]);
git(w, &["branch", "feature"]);
};
let ran = for_each_driver(
&h,
&build,
&|driver, fetch, served, local_git| {
let wildcard = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &wildcard)
.unwrap_or_else(|e| panic!("[{}] initial DF fetch failed: {e}", driver.name));
assert!(
local_git.join("refs/remotes/origin/feature").is_file(),
"[{}] origin/feature must be a loose ref file before the conflict",
driver.name
);
git(served, &["branch", "-D", "feature"]);
git(served, &["branch", "feature/sub", "refs/heads/main"]);
let sub_oid = rev_parse(served, "refs/heads/feature/sub");
let pruning = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
prune: true,
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &pruning).unwrap_or_else(|e| {
panic!(
"[{}] prune-before-update D/F fetch failed (D/F conflict not resolved?): {e}",
driver.name
)
});
assert!(
resolve_ref(local_git, "refs/remotes/origin/feature").is_err(),
"[{}] stale origin/feature ref must be pruned",
driver.name
);
assert_eq!(
resolve_ref(local_git, "refs/remotes/origin/feature/sub").unwrap_or_else(|_| {
panic!(
"[{}] origin/feature/sub must be written after the prune cleared the D/F",
driver.name
)
}),
sub_oid,
"[{}] feature/sub oid",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for the D/F conflict test");
}
#[test]
fn matrix_empty_unborn_remote() {
let h = Harness::new();
let build = |w: &Path| {
git(w, &["init", "-q", "-b", "main", "."]);
};
let ran = for_each_driver(
&h,
&build,
&|driver, fetch, _served, local_git| {
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::All,
..Default::default()
};
let outcome = fetch(local_git, &opts).unwrap_or_else(|e| {
panic!("[{}] empty-remote fetch failed (should be a clean no-op): {e}", driver.name)
});
assert!(
outcome.updates.iter().all(|u| u.new_oid.is_none()),
"[{}] unborn remote must yield no ref creations; got {:?}",
driver.name,
outcome.updates
);
assert!(
resolve_ref(local_git, "refs/remotes/origin/main").is_err(),
"[{}] no tracking ref should be written for an unborn remote",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for the empty-remote test");
}
#[test]
fn matrix_incremental_shared_history() {
let h = Harness::new();
let ran = for_each_driver(
&h,
&|w| build_source_work(w),
&|driver, fetch, served, local_git| {
let seed = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &seed)
.unwrap_or_else(|e| panic!("[{}] seed fetch failed: {e}", driver.name));
let local_root = local_git.parent().unwrap();
git(local_root, &["branch", "base", "refs/remotes/origin/main"]);
let shared_main = rev_parse(served, "refs/heads/main");
let odb_before = open_odb(local_git);
assert!(
odb_before.exists(&shared_main),
"[{}] shared main must be present after seed",
driver.name
);
let work2 = h.path().join(format!("inc-work-{}", driver.name.replace('/', "-")));
git_clone_nonbare(served, &work2);
git(&work2, &["checkout", "-q", "-B", "topic", "origin/topic"]);
std::fs::write(work2.join("c.txt"), "three\n").unwrap();
git(&work2, &["add", "c.txt"]);
git(&work2, &["commit", "-q", "-m", "c3"]);
git(&work2, &["push", "-q", "origin", "topic"]);
let topic_new = rev_parse(&work2, "refs/heads/topic");
let inc = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch(local_git, &inc)
.unwrap_or_else(|e| panic!("[{}] incremental fetch failed: {e}", driver.name));
let got_topic = resolve_ref(local_git, "refs/remotes/origin/topic")
.unwrap_or_else(|_| panic!("[{}] origin/topic missing after incremental", driver.name));
assert_eq!(
got_topic, topic_new,
"[{}] incremental must advance origin/topic to the new tip",
driver.name
);
let odb_after = open_odb(local_git);
assert!(
odb_after.exists(&topic_new),
"[{}] new topic commit {} missing after incremental fetch",
driver.name,
topic_new.to_hex()
);
assert_eq!(
resolve_ref(local_git, "refs/remotes/origin/main").unwrap().to_hex(),
git(served, &["rev-parse", "refs/heads/main"]).trim(),
"[{}] shared main unchanged after incremental",
driver.name
);
fsck_clean(local_root);
},
);
assert!(ran > 0, "no transport driver was available for the incremental test");
}
fn git_clone_nonbare(bare: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
let st = Command::new("git")
.args(["clone", "-q", bare.to_str().unwrap(), dst.to_str().unwrap()])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "T")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_COMMITTER_NAME", "T")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.status()
.expect("git clone for incremental setup");
assert!(st.success(), "git clone of served bare repo failed");
}
#[test]
fn matrix_head_symref_nondefault_branch() {
let h = Harness::new();
let build = |w: &Path| {
build_source_work(w);
};
let ran = for_each_driver_custom_serve(
&h,
&build,
&|served| {
git(served, &["symbolic-ref", "HEAD", "refs/heads/topic"]);
},
&|driver, fetch, served, local_git| {
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
let outcome = fetch(local_git, &opts)
.unwrap_or_else(|e| panic!("[{}] symref fetch failed: {e}", driver.name));
assert_eq!(
outcome.default_branch.as_deref(),
Some("topic"),
"[{}] default_branch must follow the remote HEAD symref -> topic",
driver.name
);
assert_eq!(
resolve_ref(local_git, "refs/remotes/origin/topic").unwrap().to_hex(),
git(served, &["rev-parse", "refs/heads/topic"]).trim(),
"[{}] topic tip",
driver.name
);
fsck_clean(local_git.parent().unwrap());
},
);
assert!(ran > 0, "no transport driver was available for the HEAD-symref test");
}
fn for_each_driver_custom_serve(
h: &Harness,
build_source: &dyn Fn(&Path),
tweak_served: &dyn Fn(&Path),
body: &dyn Fn(&Driver, &dyn Fn(&Path, &FetchOptions) -> GritResult<FetchOutcome>, &Path, &Path),
) -> usize {
let mut ran = 0usize;
for (i, driver) in h.drivers().into_iter().enumerate() {
let src = h.make_source(
&format!("srvc-{}-{i}", driver.name.replace('/', "-")),
build_source,
);
let Some((handle, url)) = (driver.serve)(&src) else {
eprintln!("SKIP driver {}: server bring-up failed", driver.name);
continue;
};
let served = if handle._proc.is_some() {
handle.base.join("repo.git")
} else {
src.clone()
};
tweak_served(&served);
let local_root = h
.path()
.join(format!("localc-{}-{i}", driver.name.replace('/', "-")));
let local_git = init_local(&local_root);
let bound = |lg: &Path, opts: &FetchOptions| (driver.fetch)(&url, lg, opts);
body(&driver, &bound, &served, &local_git);
ran += 1;
drop(handle);
}
ran
}