Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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)
    );
}