//! A real git component pushes an incompressible commit through the hook to native git's HTTP backend. use sausage::{ exec::{Command, Exec, Options}, process::Runtime, world::World, }; use std::{ io::Write, path::Path, process::Stdio, sync::{Arc, OnceLock}, }; fn git(cwd: &Path, args: &[&str]) { assert!( std::process::Command::new("git") .args(args) .current_dir(cwd) .status() .unwrap() .success() ); } #[tokio::test] async fn several_mb_git_push() { static BUILT: OnceLock = OnceLock::new(); let component = BUILT.get_or_init(|| { let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("hook-git"); let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland"); if !userland.join("cwd-init/init.o").is_file() { assert!( std::process::Command::new("nu") .args(["--no-config-file", "cwd-init/build.nu"]) .current_dir(&userland) .status() .unwrap() .success() ); } let manifest = userland.join("Cargo.toml"); assert!( std::process::Command::new("cargo") .args(["build", "--release", "-p", "git", "--manifest-path"]) .arg(manifest) .current_dir(&userland) .args(["--target", "wasm32-wasip2", "--target-dir"]) .arg(&target) .args(["-j", "2", "--locked"]) .status() .unwrap() .success() ); target.join("wasm32-wasip2/release/git.wasm") }); let temp = tempfile::tempdir().unwrap(); let world = World::open(temp.path()).unwrap(); let root = temp.path().join("world"); std::fs::create_dir_all(root.join("bin")).unwrap(); std::fs::copy(component, root.join("bin/git.wasm")).unwrap(); let repo = root.join("repo"); std::fs::create_dir_all(&repo).unwrap(); git(&repo, &["init", "-b", "main"]); let mut data = vec![0; 4 * 1024 * 1024]; openssl::rand::rand_bytes(&mut data).unwrap(); std::fs::write(repo.join("large"), data).unwrap(); git(&repo, &["add", "large"]); git( &repo, &[ "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "large commit", ], ); let remote = temp.path().join("remote"); std::fs::create_dir_all(&remote).unwrap(); git(&remote, &["init", "--bare", "destination.git"]); git( &remote.join("destination.git"), &["config", "http.receivepack", "true"], ); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); git( &repo, &[ "remote", "add", "origin", &format!("http://127.0.0.1:{port}/destination.git"), ], ); let uploaded = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let counter = uploaded.clone(); let server = tokio::spawn(async move { loop { let (socket, _) = listener.accept().await.unwrap(); let remote = remote.clone(); let counter = counter.clone(); let service = hyper::service::service_fn(move |request: http::Request| { let remote = remote.clone(); let counter = counter.clone(); async move { use http_body_util::{BodyExt, Full}; let (parts, body) = request.into_parts(); let body = body.collect().await.unwrap().to_bytes(); counter.fetch_add(body.len(), std::sync::atomic::Ordering::Relaxed); let response = tokio::task::spawn_blocking(move || { let mut process = std::process::Command::new("git") .arg("http-backend") .env("GIT_PROJECT_ROOT", remote) .env("GIT_HTTP_EXPORT_ALL", "1") .env("REQUEST_METHOD", parts.method.as_str()) .env("PATH_INFO", parts.uri.path()) .env("QUERY_STRING", parts.uri.query().unwrap_or("")) .env( "CONTENT_TYPE", parts .headers .get("content-type") .map(|h| h.to_str().unwrap()) .unwrap_or(""), ) .env("CONTENT_LENGTH", body.len().to_string()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); let mut stdin = process.stdin.take().unwrap(); let output = std::thread::scope(|scope| { scope.spawn(move || { stdin.write_all(&body).unwrap(); }); process.wait_with_output().unwrap() }); assert!(output.status.success()); let split = output .stdout .windows(4) .position(|b| b == b"\r\n\r\n") .unwrap(); let headers = std::str::from_utf8(&output.stdout[..split]).unwrap(); let mut response = http::Response::builder(); for line in headers.split("\r\n") { let (name, value) = line.split_once(':').unwrap(); if name.eq_ignore_ascii_case("status") { response = response.status( value .trim() .split(' ') .next() .unwrap() .parse::() .unwrap(), ); } else { response = response.header(name, value.trim()); } } response .body(Full::new(bytes::Bytes::copy_from_slice( &output.stdout[split + 4..], ))) .unwrap() }) .await .unwrap(); Ok::<_, std::convert::Infallible>(response) } }); hyper::server::conn::http1::Builder::new() .keep_alive(false) .serve_connection(hyper_util::rt::TokioIo::new(socket), service) .await .unwrap(); } }); let runtime = Runtime::new(world).unwrap(); let mut output = Vec::new(); let code = runtime .exec( Command::Argv(vec![ "/bin/git.wasm".into(), "push".into(), "origin".into(), "main".into(), ]), Options { cwd: "/repo".into(), timeout: Some(std::time::Duration::from_secs(60)), ..Default::default() }, &mut |_, b| output.extend_from_slice(b), ) .await .unwrap(); server.abort(); assert_eq!(code, 0, "{}", String::from_utf8_lossy(&output)); assert!(uploaded.load(std::sync::atomic::Ordering::Relaxed) > 3 * 1024 * 1024); git( &temp.path().join("remote/destination.git"), &["cat-file", "-e", "refs/heads/main:large"], ); }