mod common;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::pin::Pin;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
use bytes::Bytes;
use futures::io::AsyncWrite as FuturesAsyncWrite;
use gix_hash::ObjectId;
use tokio::io::{copy_bidirectional, duplex, AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use alkgit::{
serve_duplex_resolved, serve_stateless_advertisement, serve_stateless_post, AccessAction,
AuthorizedRepo, BodyReader, GitPackGen, GitPackIngest, GitRefs, GitRegistry, GitRegistryStore,
Limits, RefUpdate, RepoRecord, Service, SubstrateError, Visibility,
};
use common::{
blob_fixture, commit_fixture, tag_fixture, tree_fixture, MemObjects, MemPackGen, MemPackIngest,
MemRefs, MemRegistry,
};
struct Fixture {
registry: MemRegistry,
refs: MemRefs,
gen: Arc<MemPackGen>,
ingest: Arc<MemPackIngest>,
commit_b: ObjectId,
tag_v1: ObjectId,
commit_c: ObjectId,
unborn_repo_id: &'static str,
}
async fn fixture() -> Fixture {
let repo = RepoRecord {
repo_id: "owner/doubled".to_owned(),
visibility: Visibility::Public,
grants: BTreeMap::new(),
};
let registry = MemRegistry::default();
registry
.put_repo(&repo)
.await
.expect("the fixture repo registers");
let unborn_repo_id = "owner/unborn";
registry
.put_repo(&RepoRecord {
repo_id: unborn_repo_id.to_owned(),
visibility: Visibility::Public,
grants: BTreeMap::new(),
})
.await
.expect("the unborn fixture repo registers");
let refs = MemRefs::default();
refs.set_head_symref(
&RepoRecord {
repo_id: unborn_repo_id.to_owned(),
visibility: Visibility::Public,
grants: BTreeMap::new(),
},
Some("refs/heads/main".to_owned()),
);
let objects = MemObjects::default();
let gen = Arc::new(MemPackGen {
objects: objects.clone(),
});
let ingest = Arc::new(MemPackIngest {
objects: objects.clone(),
});
let (blob_a, blob_a_bytes) = blob_fixture(b"alpha");
let (blob_b, blob_b_bytes) = blob_fixture(b"beta");
let (tree_a, tree_a_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "a.txt", blob_a)]);
let (tree_b, tree_b_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "b.txt", blob_b)]);
let (commit_a, commit_a_bytes) = commit_fixture(tree_a, &[], "alpha");
let (commit_b, commit_b_bytes) = commit_fixture(tree_b, &[commit_a], "beta");
let (tag_v1, tag_bytes) = tag_fixture(commit_b, gix_object::Kind::Commit, "v1");
for (id, bytes) in [
(blob_a, blob_a_bytes),
(blob_b, blob_b_bytes),
(tree_a, tree_a_bytes),
(tree_b, tree_b_bytes),
(commit_a, commit_a_bytes),
(commit_b, commit_b_bytes),
(tag_v1, tag_bytes),
] {
objects.put(&repo, id, bytes);
}
refs.apply_updates(
&repo,
vec![
RefUpdate {
name: "refs/heads/main".to_owned(),
expected: None,
new: Some(commit_b),
},
RefUpdate {
name: "refs/heads/old".to_owned(),
expected: None,
new: Some(commit_a),
},
RefUpdate {
name: "refs/tags/v1".to_owned(),
expected: None,
new: Some(tag_v1),
},
],
false,
)
.await
.expect("the fixture refs apply");
let (blob_c, blob_c_bytes) = blob_fixture(b"gamma");
let (tree_c, tree_c_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "c.txt", blob_c)]);
let (commit_c, commit_c_bytes) = commit_fixture(tree_c, &[commit_b], "gamma");
for (id, bytes) in [
(blob_c, blob_c_bytes),
(tree_c, tree_c_bytes),
(commit_c, commit_c_bytes),
] {
objects.put(&repo, id, bytes);
}
Fixture {
registry,
refs,
gen,
ingest,
commit_b,
tag_v1,
commit_c,
unborn_repo_id,
}
}
struct HeadRefs {
inner: MemRefs,
head_oid: ObjectId,
head_target: String,
unborn_repo: Option<String>,
}
#[async_trait::async_trait]
impl GitRefs for HeadRefs {
async fn list_refs(
&self,
repo: &RepoRecord,
) -> Result<Vec<alkgit::RefLine>, alkgit::StorageError> {
let mut listed = self.inner.list_refs(repo).await?;
if self
.unborn_repo
.as_deref()
.is_some_and(|id| id == repo.repo_id)
{
return Ok(listed);
}
listed.insert(
0,
alkgit::RefLine {
name: "HEAD".to_owned(),
oid: Some(self.head_oid),
peeled: None,
symref_target: Some(self.head_target.clone()),
},
);
Ok(listed)
}
async fn apply_updates(
&self,
repo: &RepoRecord,
updates: Vec<RefUpdate>,
atomic: bool,
) -> Result<Vec<alkgit::RefOutcome>, alkgit::StorageError> {
self.inner.apply_updates(repo, updates, atomic).await
}
}
async fn spawn_daemon_door(fx: &Fixture) -> (std::net::SocketAddr, JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("the door binds");
let addr = listener.local_addr().expect("the door's local addr");
let registry: Arc<dyn GitRegistry> = Arc::new(MemRegistry::clone(&fx.registry));
let refs: Arc<dyn GitRefs> = Arc::new(HeadRefs {
inner: MemRefs::clone(&fx.refs),
head_oid: fx.commit_b,
head_target: "refs/heads/main".to_owned(),
unborn_repo: Some(fx.unborn_repo_id.to_owned()),
});
let gen: Arc<dyn GitPackGen> = fx.gen.clone();
let ingest: Arc<dyn GitPackIngest> = fx.ingest.clone();
let limits = Limits::default();
let handle = tokio::spawn(async move {
loop {
let Ok((mut conn, _peer)) = listener.accept().await else {
break;
};
let registry = Arc::clone(®istry);
let refs = Arc::clone(&refs);
let gen = Arc::clone(&gen);
let ingest = Arc::clone(&ingest);
tokio::spawn(async move {
let Some(frame) = read_daemon_frame(&mut conn).await else {
return;
};
let Some(repo_id) = parse_daemon_frame(&frame) else {
return;
};
let (mut pair_client, pair_server) = duplex(1 << 20);
let (relay, session) = tokio::join!(
copy_bidirectional(&mut conn, &mut pair_client),
serve_duplex_resolved(
&repo_id,
None,
Service::UploadPack,
pair_server,
limits,
registry,
refs,
gen,
ingest,
false,
Arc::new(Semaphore::new(limits.max_concurrent_blocking_tasks)),
)
);
let _ = relay;
if let Err(error) = session {
eprintln!("the duplex session returned: {error:?}");
}
});
}
});
(addr, handle)
}
async fn read_daemon_frame(conn: &mut TcpStream) -> Option<Vec<u8>> {
let mut header = [0u8; 4];
tokio::time::timeout(Duration::from_secs(10), conn.read_exact(&mut header))
.await
.ok()?
.ok()?;
let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
.expect("hex length");
if !(4..=1 << 16).contains(&len) {
return None;
}
let mut payload = vec![0u8; len - 4];
tokio::time::timeout(Duration::from_secs(10), conn.read_exact(&mut payload))
.await
.ok()?
.ok()?;
Some(payload)
}
fn parse_daemon_frame(frame: &[u8]) -> Option<String> {
let text = String::from_utf8(frame.to_vec()).ok()?;
let mut segments = text.split('\0');
let (service, repo_path) = segments.next()?.split_once(' ')?;
if service != "git-upload-pack" {
return None;
}
let repo_id = repo_path.strip_prefix('/').unwrap_or(repo_path);
if repo_id.is_empty() || !segments.any(|segment| segment == "version=2") {
return None;
}
Some(repo_id.to_owned())
}
async fn spawn_http_door(fx: &Fixture) -> (std::net::SocketAddr, JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("the door binds");
let addr = listener.local_addr().expect("the door's local addr");
let registry: Arc<dyn GitRegistry> = Arc::new(MemRegistry::clone(&fx.registry));
let repo_ids = ["owner/doubled".to_owned(), fx.unborn_repo_id.to_owned()]
.into_iter()
.collect::<std::collections::BTreeSet<String>>();
let refs: Arc<dyn GitRefs> = Arc::new(HeadRefs {
inner: MemRefs::clone(&fx.refs),
head_oid: fx.commit_b,
head_target: "refs/heads/main".to_owned(),
unborn_repo: Some(fx.unborn_repo_id.to_owned()),
});
let gen: Arc<dyn GitPackGen> = fx.gen.clone();
let ingest: Arc<dyn GitPackIngest> = fx.ingest.clone();
let limits = Limits::default();
let handle = tokio::spawn(async move {
loop {
let Ok((mut conn, _peer)) = listener.accept().await else {
break;
};
let registry = Arc::clone(®istry);
let refs = Arc::clone(&refs);
let gen = Arc::clone(&gen);
let ingest = Arc::clone(&ingest);
let repo_ids = repo_ids.clone();
tokio::spawn(async move {
let mut buffer: Vec<u8> = Vec::new();
loop {
let Some(mut request) = read_http_request(&mut conn, &mut buffer).await else {
return;
};
let Some(repo_id) = request
.path
.as_deref()
.and_then(repo_id_from_path)
.filter(|id| repo_ids.contains(id))
else {
return;
};
let Ok(record) = registry.resolve(&repo_id).await else {
return;
};
let Some(marker) = AuthorizedRepo::authorize(record, None, AccessAction::Read)
else {
return;
};
match serve_http_request(
&mut conn,
&mut request,
&marker,
&refs,
&gen,
&ingest,
limits,
)
.await
{
Some(true) => {}
_ => return,
}
}
});
}
});
(addr, handle)
}
fn repo_id_from_path(path: &str) -> Option<String> {
let trimmed = path.strip_prefix('/')?;
let rest = trimmed
.strip_suffix("/info/refs")
.or_else(|| trimmed.strip_suffix("/git-upload-pack"))?;
if rest.is_empty() {
return None;
}
Some(rest.to_owned())
}
struct HttpRequest {
method: String,
path: Option<String>,
query: Option<String>,
body: Vec<u8>,
}
async fn read_http_request(conn: &mut TcpStream, buffer: &mut Vec<u8>) -> Option<HttpRequest> {
let mut chunk = [0u8; 4096];
let head_end = loop {
if let Some(position) = buffer.windows(4).position(|window| window == b"\r\n\r\n") {
break position;
}
let read = conn.read(&mut chunk).await.ok()?;
if read == 0 {
return None;
}
buffer.extend_from_slice(&chunk[..read]);
if buffer.len() > 64 * 1024 {
return None;
}
};
let head_text = String::from_utf8(buffer[..head_end].to_vec()).ok()?;
let mut lines = head_text.split("\r\n");
let request_line = lines.next()?;
let mut parts = request_line.split(' ');
let method = parts.next()?.to_owned();
let path = parts.next()?;
let (path, query) = match path.split_once('?') {
Some((path, query)) => (path.to_owned(), Some(query.to_owned())),
None => (path.to_owned(), None),
};
let mut content_length: Option<u64> = None;
for header in lines {
let Some((name, value)) = header.split_once(':') else {
continue;
};
let name = name.trim().to_ascii_lowercase();
let value = value.trim();
if name == "content-length" {
content_length = value.parse().ok();
}
}
let body_start = head_end + 4;
let body_len = content_length.unwrap_or(0) as usize;
while buffer.len() < body_start + body_len {
let read = conn.read(&mut chunk).await.ok()?;
if read == 0 {
return None;
}
buffer.extend_from_slice(&chunk[..read]);
}
let body = buffer[body_start..body_start + body_len].to_vec();
buffer.drain(..body_start + body_len);
Some(HttpRequest {
method,
path: Some(path),
query,
body,
})
}
async fn serve_http_request(
conn: &mut TcpStream,
request: &mut HttpRequest,
marker: &AuthorizedRepo,
refs: &Arc<dyn GitRefs>,
gen: &Arc<dyn GitPackGen>,
ingest: &Arc<dyn GitPackIngest>,
limits: Limits,
) -> Option<bool> {
let path = request.path.clone()?;
if request.method == "GET" {
if !path.ends_with("/info/refs")
|| request.query.as_deref() != Some("service=git-upload-pack")
{
return None;
}
let sink = VecSink::default();
serve_stateless_advertisement(
marker,
sink.clone(),
Service::UploadPack,
Arc::clone(refs),
&limits,
false,
)
.await
.ok()?;
respond(
conn,
"application/x-git-upload-pack-advertisement",
&sink.body(),
)
.await;
Some(true)
} else if request.method == "POST" {
if !path.ends_with("/git-upload-pack") {
return None;
}
let body = std::mem::take(&mut request.body);
let reader = BodyReader::new(futures::stream::iter(vec![Ok::<
Bytes,
std::convert::Infallible,
>(Bytes::from(body))]));
let sink = VecSink::default();
serve_stateless_post(
marker,
reader,
sink.clone(),
limits,
Service::UploadPack,
Arc::clone(refs),
Arc::clone(gen),
Arc::clone(ingest),
false,
Arc::new(Semaphore::new(limits.max_concurrent_blocking_tasks)),
)
.await
.ok()?;
respond(conn, "application/x-git-upload-pack-result", &sink.body()).await;
Some(true)
} else {
None
}
}
async fn respond(conn: &mut TcpStream, content_type: &str, body: &[u8]) {
let head = format!(
"HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nCache-Control: no-cache\r\nContent-Length: {}\r\n\r\n",
body.len()
);
conn.write_all(head.as_bytes())
.await
.expect("the door writes the head");
conn.write_all(body)
.await
.expect("the door writes the body");
conn.flush().await.expect("the door flushes the body");
}
#[derive(Clone, Default)]
struct VecSink(Arc<StdMutex<Vec<u8>>>);
impl VecSink {
fn body(&self) -> Vec<u8> {
self.0.lock().expect("sink is not poisoned").clone()
}
}
impl FuturesAsyncWrite for VecSink {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
self.0
.lock()
.expect("sink is not poisoned")
.extend_from_slice(buf);
std::task::Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_close(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
self.poll_flush(cx)
}
}
#[allow(unused)]
fn session_error_note(error: SubstrateError) -> SubstrateError {
error
}
fn workspace(tag: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("alkgit-real-client-{}-{}", std::process::id(), tag));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("the workspace creates");
dir
}
fn git_env() -> Vec<(String, String)> {
vec![
("GIT_CONFIG_NOSYSTEM".to_owned(), "1".to_owned()),
("GIT_CONFIG_GLOBAL".to_owned(), "/dev/null".to_owned()),
("GIT_TERMINAL_PROMPT".to_owned(), "0".to_owned()),
]
}
fn git_command() -> Option<tokio::process::Command> {
let mut command = tokio::process::Command::new("git");
command
.env_clear()
.envs(git_env())
.env("PATH", std::env::var("PATH").unwrap_or_default());
Some(command)
}
fn expect_git_command() -> tokio::process::Command {
git_command().expect("the git binary resolved earlier in the row")
}
async fn assert_clone_completed(dst: &PathBuf, expected_head: ObjectId, tag: ObjectId) {
let output = expect_git_command()
.arg("-C")
.arg(dst)
.args(["rev-parse", "HEAD"])
.stdin(Stdio::null())
.output()
.await
.expect("rev-parse runs");
assert!(
output.status.success(),
"rev-parse HEAD in the clone failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
expected_head.to_string(),
"the clone's HEAD is the expected branch tip"
);
let output = expect_git_command()
.arg("-C")
.arg(dst)
.args(["cat-file", "-t"])
.arg(tag.to_string())
.stdin(Stdio::null())
.output()
.await
.expect("cat-file runs");
assert!(
output.status.success(),
"the annotated tag did not resolve in the clone: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"tag",
"the tag resolves as an annotated tag object"
);
}
async fn assert_fetch_completed(dst: &PathBuf, expected: ObjectId) {
let mut command = expect_git_command();
let output = command
.arg("-C")
.arg(dst)
.args(["rev-parse", "refs/remotes/origin/main"])
.stdin(Stdio::null())
.output()
.await
.expect("rev-parse runs");
assert!(
output.status.success(),
"the tracking ref did not resolve: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
expected.to_string(),
"the tracking ref advanced to the new tip"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_real_git_clone_completes_over_the_duplex_git_daemon_door() {
let git = git_command();
if git.is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let _ = git;
let fx = fixture().await;
let (addr, door) = spawn_daemon_door(&fx).await;
let dir = workspace("duplex-clone");
let mut command = expect_git_command();
let output = command
.args(["-c", "protocol.version=2", "clone"])
.arg(format!("git://{addr}/owner/doubled"))
.arg(dir.join("dst"))
.stdin(Stdio::null())
.output()
.await
.expect("the clone runs");
assert!(
output.status.success(),
"the real clone failed over the duplex door: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_clone_completed(&dir.join("dst"), fx.commit_b, fx.tag_v1).await;
door.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_real_configured_fetch_sends_include_tag_and_completes_over_the_duplex_door() {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let fx = fixture().await;
let (addr, door) = spawn_daemon_door(&fx).await;
let dir = workspace("duplex-fetch");
let mut command = expect_git_command();
let output = command
.args(["-c", "protocol.version=2", "clone"])
.arg(format!("git://{addr}/owner/doubled"))
.arg(dir.join("dst"))
.stdin(Stdio::null())
.output()
.await
.expect("the clone runs");
assert!(
output.status.success(),
"the real clone failed over the duplex door: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
fx.refs
.apply_updates(
&RepoRecord {
repo_id: "owner/doubled".to_owned(),
visibility: Visibility::Public,
grants: BTreeMap::new(),
},
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: Some(fx.commit_b),
new: Some(fx.commit_c),
}],
false,
)
.await
.expect("the fixture's main advances");
let mut command = expect_git_command();
let output = command
.env("GIT_TRACE_PACKET", "1")
.args([
"-C",
dir.join("dst").to_str().unwrap_or_default(),
"fetch",
"origin",
])
.stdin(Stdio::null())
.output()
.await
.expect("the fetch runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real configured fetch failed over the duplex door: {trace}"
);
assert!(
trace.contains("include-tag"),
"the real client's negotiation carried include-tag (the trace was captured)"
);
assert!(
!trace.contains("session-id"),
"the capable client sent no session-id against the honest advertisement"
);
assert_clone_completed(&dir.join("dst"), fx.commit_b, fx.tag_v1).await;
door.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_real_client_with_advertise_sid_sends_no_session_id_over_the_duplex_door() {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let fx = fixture().await;
let (addr, door) = spawn_daemon_door(&fx).await;
let dir = workspace("duplex-sid");
let mut command = expect_git_command();
let output = command
.args([
"-c",
"protocol.version=2",
"-c",
"transfer.advertiseSID=true",
"clone",
])
.arg(format!("git://{addr}/owner/doubled"))
.arg(dir.join("dst"))
.env("GIT_TRACE_PACKET", "1")
.stdin(Stdio::null())
.output()
.await
.expect("the clone runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real clone failed over the duplex door: {trace}"
);
assert!(
!trace.contains("session-id"),
"the capable client sent no session-id against the honest advertisement"
);
assert_clone_completed(&dir.join("dst"), fx.commit_b, fx.tag_v1).await;
door.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_real_client_sessions_complete_over_the_stateless_http_door() {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let fx = fixture().await;
let (addr, door) = spawn_http_door(&fx).await;
let url = format!("http://{addr}/owner/doubled");
let dir = workspace("http-clone");
let mut command = expect_git_command();
let output = command
.args(["-c", "protocol.version=2", "clone", &url])
.arg(dir.join("dst"))
.stdin(Stdio::null())
.output()
.await
.expect("the clone runs");
assert!(
output.status.success(),
"the real clone failed over the stateless door: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_clone_completed(&dir.join("dst"), fx.commit_b, fx.tag_v1).await;
fx.refs
.apply_updates(
&RepoRecord {
repo_id: "owner/doubled".to_owned(),
visibility: Visibility::Public,
grants: BTreeMap::new(),
},
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: Some(fx.commit_b),
new: Some(fx.commit_c),
}],
false,
)
.await
.expect("the fixture's main advances");
let mut command = expect_git_command();
let output = command
.env("GIT_TRACE_PACKET", "1")
.args([
"-C",
dir.join("dst").to_str().unwrap_or_default(),
"fetch",
"origin",
])
.stdin(Stdio::null())
.output()
.await
.expect("the fetch runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real configured fetch failed over the stateless door: {trace}"
);
assert!(
trace.contains("include-tag"),
"the real client's negotiation carried include-tag over the stateless door"
);
assert!(
!trace.contains("session-id"),
"the client sent no session-id against the honest advertisement"
);
assert_fetch_completed(&dir.join("dst"), fx.commit_c).await;
let dir = workspace("http-sid");
let mut command = expect_git_command();
let output = command
.args([
"-c",
"protocol.version=2",
"-c",
"transfer.advertiseSID=true",
"clone",
&url,
])
.arg(dir.join("dst"))
.env("GIT_TRACE_PACKET", "1")
.stdin(Stdio::null())
.output()
.await
.expect("the clone runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real clone failed over the stateless door: {trace}"
);
assert!(
!trace.contains("session-id"),
"the capable client sent no session-id against the honest advertisement"
);
assert_clone_completed(&dir.join("dst"), fx.commit_c, fx.tag_v1).await;
door.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_real_git_ls_remote_completes_against_the_unborn_repo_over_the_duplex_door() {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let fx = fixture().await;
let (addr, door) = spawn_daemon_door(&fx).await;
let mut command = expect_git_command();
let output = command
.env("GIT_TRACE_PACKET", "1")
.args(["-c", "protocol.version=2", "ls-remote"])
.arg(format!("git://{addr}/owner/unborn"))
.stdin(Stdio::null())
.output()
.await
.expect("the ls-remote runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real ls-remote failed against the unborn repo over the duplex door: {trace}"
);
assert!(
trace.contains("ls-remote> unborn"),
"the real client requested the unborn argument (the capture-session fact)"
);
assert!(
trace.contains("symref-target:refs/heads/main"),
"the unborn HEAD symref line was served: {trace}"
);
door.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_real_git_ls_remote_completes_against_the_unborn_repo_over_the_stateless_door() {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let fx = fixture().await;
let (addr, door) = spawn_http_door(&fx).await;
let url = format!("http://{addr}/owner/unborn");
let mut command = expect_git_command();
let output = command
.env("GIT_TRACE_PACKET", "1")
.args(["-c", "protocol.version=2", "ls-remote", &url])
.stdin(Stdio::null())
.output()
.await
.expect("the ls-remote runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real ls-remote failed against the unborn repo over the stateless door: {trace}"
);
assert!(
trace.contains("symref-target:refs/heads/main"),
"the unborn HEAD symref line was served over the stateless door: {trace}"
);
door.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_real_git_clone_of_the_unborn_repo_resolves_the_default_branch_over_the_duplex_door() {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return;
}
let fx = fixture().await;
let (addr, door) = spawn_daemon_door(&fx).await;
let dir = workspace("duplex-unborn-clone");
let mut command = expect_git_command();
let output = command
.env("GIT_TRACE_PACKET", "1")
.args(["-c", "protocol.version=2", "clone"])
.arg(format!("git://{addr}/owner/unborn"))
.arg(dir.join("dst"))
.stdin(Stdio::null())
.output()
.await
.expect("the clone runs");
let trace = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"the real clone of the unborn repo failed over the duplex door: {trace}"
);
assert!(
trace.contains("symref-target:refs/heads/main"),
"the unborn HEAD symref line rode the ls-refs round: {trace}"
);
assert!(
trace.contains("You appear to have cloned an empty repository"),
"the real client recognized the empty repository"
);
let head_path = dir.join("dst/.git/HEAD");
let head = std::fs::read_to_string(&head_path).expect("the clone's HEAD reads");
assert!(
head.contains("ref: refs/heads/main"),
"the clone's HEAD points at the symref target's branch: {head}"
);
door.abort();
}