crates/gunnar-cli/tests/ls_remote_verb.rs
#![cfg(all(feature = "server", feature = "client"))]
//! **`gunnar ls-remote` against a running `gunnar serve`, diffed against
//! `git ls-remote` over the same SSH connection.**
//!
//! `gunnar_client::ls_remote` and `ls_remote_with` had existed since the client
//! did and nothing exposed them on the command line. It is the first question a
//! user asks of a remote — *is it reachable, does my key work, does it hold what
//! I think* — and until now the only way to ask a gunnar server was to clone it.
//!
//! # Why this file exists as well as `gunnar-client`'s pipe test
//!
//! `crates/gunnar-client/tests/ls_remote_matches_git.rs` diffs the same output
//! against `git ls-remote` over a pair of pipes to a real `git upload-pack`. It
//! proves the rendering and the peeling. It cannot prove **two** things this does:
//!
//! 1. the `ls-remote` **verb** — clap's subcommand name, the dispatch arm, and
//! the bytes the binary actually writes to stdout;
//! 2. `ls_remote_with`, the **SSH** path, which stopped delegating to
//! `nornir_git::ssh::ls_remote_blocking_auth` on 2026-08-05 and now runs over
//! `connect_upload_pack` + gix's `LsRefsCommand`. That rewiring is why the
//! peeled `^{}` line exists at all — nornir-git's hand-rolled parser kept the
//! first two tokens of each `ls-refs` line and dropped `peeled:` — and a
//! change of transport that nothing exercises is a change nobody has checked.
//!
//! The remote here is gunnar's own `upload-pack`, not stock git's, so the
//! comparison is also gunnar-the-server answering gunnar-the-client and stock git
//! identically on the same wire.
mod common;
use std::process::Command;
use common::{local_repo, tools_present, Harness};
use gunnar_core::HashKind;
/// Push a repository holding an annotated tag and a lightweight one, and return
/// its URL.
///
/// Both tags, because they are the pair that tells a working `ls-remote` from a
/// broken one: the annotated tag must print its **tag object** and a second
/// `^{}` line for the commit under it, the lightweight tag must print one line,
/// and on a lightweight tag the two objects are equal so a fixture with only
/// those cannot tell the cases apart at all.
fn seeded_remote(harness: &Harness, hash: HashKind) -> String {
let work = harness.dir.path().join("work");
local_repo(harness, &work, hash, "gunnar ls-remote\n");
harness
.git(&work, &["tag", "-a", "ann", "-m", "an annotated tag"])
.expect_ok("git tag -a");
harness.git(&work, &["tag", "lw"]).expect_ok("git tag");
harness
.git(&work, &["branch", "topic"])
.expect_ok("git branch");
let url = harness.url("team", "project");
harness
.git(&work, &["push", &url, "--all"])
.expect_ok("git push --all to gunnar over ssh");
harness
.git(&work, &["push", &url, "--tags"])
.expect_ok("git push --tags to gunnar over ssh");
url
}
/// `gunnar ls-remote <url>` as the binary runs it, stdout verbatim.
///
/// `HOME` is the harness's temporary directory: the client's default host-key
/// policy is trust-on-first-use, and it must write its `known_hosts` there rather
/// than into the machine's.
fn gunnar_ls_remote(harness: &Harness, url: &str) -> String {
let out = Command::new(env!("CARGO_BIN_EXE_gunnar-cli"))
.args(["ls-remote", "-i"])
.arg(&harness.key_path)
.arg(url)
.env("HOME", harness.dir.path())
.env("SSH_AUTH_SOCK", "")
.output()
.expect("run the gunnar binary");
assert!(
out.status.success(),
"`gunnar ls-remote {url}` exited {:?}\nstdout: {}\nstderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
String::from_utf8(out.stdout).expect("ls-remote output is utf-8")
}
fn matches_git(hash: HashKind) {
if !tools_present() {
eprintln!("SKIP: git or ssh missing");
return;
}
let harness = Harness::start("team", "project", hash);
let url = seeded_remote(&harness, hash);
let theirs = harness.git(harness.dir.path(), &["ls-remote", &url]);
theirs.expect_ok("git ls-remote against gunnar");
let theirs = theirs.stdout.clone();
let ours = gunnar_ls_remote(&harness, &url);
// Anti-vacuity before the diff: two empty strings are identical and prove
// nothing. HEAD, two branches, two tags, one of them peeled — six lines.
assert_eq!(
theirs.lines().count(),
6,
"{hash}: git printed {} lines against gunnar, not the six this fixture \
pushed, so the comparison below measures the wrong thing:\n{theirs}",
theirs.lines().count()
);
assert!(
theirs.contains("refs/tags/ann^{}"),
"{hash}: gunnar's server advertised no peeled tag to git either, so the \
peeling half of this test is not being exercised:\n{theirs}"
);
assert_eq!(
ours, theirs,
"{hash}: `gunnar ls-remote` differs from `git ls-remote` against the same \
gunnar server.\n--- gunnar ---\n{ours}--- git ---\n{theirs}"
);
}
#[test]
fn ls_remote_matches_git_over_ssh_sha1() {
matches_git(HashKind::Sha1);
}
/// SHA-256, and not as an afterthought: an `ls-refs` request that does not echo
/// the remote's `object-format` gets an **empty** answer from a SHA-256
/// repository — which looks exactly like a repository with no refs.
#[test]
fn ls_remote_matches_git_over_ssh_sha256() {
matches_git(HashKind::Sha256);
}
/// A repository with no commits: empty output, exit 0, the way git answers.
///
/// This is the state a `gunnar ls-remote` is most likely pointed at first — a
/// repository just created on the server — and an error here would read as
/// *unreachable* for a remote that is perfectly reachable.
#[test]
fn ls_remote_on_an_empty_repository_says_nothing_and_succeeds() {
if !tools_present() {
eprintln!("SKIP: git or ssh missing");
return;
}
let harness = Harness::start("team", "project", HashKind::Sha256);
let url = harness.url("team", "project");
let theirs = harness.git(harness.dir.path(), &["ls-remote", &url]);
theirs.expect_ok("git ls-remote against an empty gunnar repository");
assert_eq!(
theirs.stdout, "",
"git printed something for an empty repository, so this test is asserting \
the wrong thing: {:?}",
theirs.stdout
);
assert_eq!(gunnar_ls_remote(&harness, &url), "");
}
/// An unreachable remote must **fail**, loudly.
///
/// Without this the three tests above are satisfied by a verb that prints nothing
/// and exits 0 no matter what it is pointed at — which is the same empty stdout
/// an empty repository produces.
#[test]
fn ls_remote_fails_on_a_remote_that_is_not_there() {
if !tools_present() {
eprintln!("SKIP: git or ssh missing");
return;
}
let harness = Harness::start("team", "project", HashKind::Sha256);
let url = harness.url("team", "nosuchrepo");
let out = Command::new(env!("CARGO_BIN_EXE_gunnar-cli"))
.args(["ls-remote", "-i"])
.arg(&harness.key_path)
.arg(&url)
.env("HOME", harness.dir.path())
.env("SSH_AUTH_SOCK", "")
.output()
.expect("run the gunnar binary");
assert!(
!out.status.success(),
"`gunnar ls-remote` on a repository that does not exist exited 0.\n\
stdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
out.stdout.is_empty(),
"it printed refs for a repository that does not exist: {}",
String::from_utf8_lossy(&out.stdout)
);
}