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/repo_verbs_against_the_door.rs

#![cfg(feature = "server")]

//! **`gunnar settings`, `gunnar create` and a sha256 tree's first `gunnar push`,
//! against the real door** (OX-WIRED 2026-10-28: the verbs were mounted, but only a
//! stub door ever answered them, in `gunnar-ssh/tests/repo_verb.rs`).
//!
//! Here the `gunnar` binary runs as a person runs it: over stock `ssh` to a real
//! `gunnar serve` whose control plane is up, so the forge's `gunnar-repo` verb is
//! answered by the server's own `Repos` handlers. Every answer is checked twice:
//! by what the verb prints, and by the catalog it changed.

mod common;

use std::path::Path;
use std::process::Command;

use common::{local_repo, tools_present, Harness, HarnessOptions, Seed};
use gunnar_core::{HashKind, RepoRef, Visibility};

/// The installed appliance (the bootstrap admin, no repository) with its control plane up
/// (the repository door answers only once the control plane is), and the namespace
/// `nordisk`: `Repos.Create` makes a repository in a namespace that exists.
fn appliance() -> Harness {
    assert!(
        tools_present(),
        "this test requires `git` and `ssh` on PATH"
    );
    let h = Harness::start_with_options(
        "unused",
        "unused",
        HashKind::Sha256,
        HarnessOptions {
            grpc: true,
            seed: Seed::BootstrapAdminOnly,
            ..HarnessOptions::default()
        },
    );
    h.stores
        .catalog()
        .create_namespace("nordisk")
        .expect("the namespace nordisk");
    h
}

/// `gunnar <args>` in `cwd`, signed in with the harness key by the `ssh` git would run.
fn gunnar(h: &Harness, cwd: &Path, args: &[&str]) -> (bool, String, String) {
    let out = Command::new(env!("CARGO_BIN_EXE_gunnar-cli"))
        .current_dir(cwd)
        .env("HOME", h.dir.path())
        .env("GIT_SSH_COMMAND", h.ssh_command())
        .env("GIT_TERMINAL_PROMPT", "0")
        .env("GIT_CONFIG_NOSYSTEM", "1")
        .env("LC_ALL", "C")
        .env_remove("SSH_AUTH_SOCK")
        .args(args)
        .output()
        .expect("the gunnar binary runs");
    (
        out.status.success(),
        String::from_utf8_lossy(&out.stdout).into_owned(),
        String::from_utf8_lossy(&out.stderr).into_owned(),
    )
}

fn host(h: &Harness) -> String {
    format!("ssh://git@127.0.0.1:{}", h.ssh_port)
}

fn catalog(h: &Harness, ns: &str, repo: &str) -> Option<gunnar_core::Repo> {
    h.stores
        .catalog()
        .get(&RepoRef::new(ns, repo).expect("legal"))
        .expect("read the catalog")
}

/// ★ `gunnar create` makes an empty repository over the door, sha256 unless `--sha1`.
#[test]
fn gunnar_create_makes_a_sha256_repository_and_a_sha1_one_when_asked() {
    let h = appliance();
    let key = h.key_path.to_str().expect("utf-8").to_owned();
    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &["create", "nordisk/knut", "--host", &host(&h), "-i", &key],
    );
    assert!(
        ok,
        "gunnar create nordisk/knut\n  stdout: {out}\n  stderr: {err}"
    );
    assert!(
        out.contains("repository nordisk/knut") && out.contains("object-format sha256"),
        "{out}"
    );
    assert_eq!(
        catalog(&h, "nordisk", "knut").expect("created").hash_kind,
        HashKind::Sha256
    );

    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &[
            "create",
            "nordisk/old",
            "--sha1",
            "--host",
            &host(&h),
            "-i",
            &key,
        ],
    );
    assert!(ok, "gunnar create --sha1\n  stdout: {out}\n  stderr: {err}");
    assert!(out.contains("object-format sha1"), "{out}");
    assert_eq!(
        catalog(&h, "nordisk", "old").expect("created").hash_kind,
        HashKind::Sha1
    );

    // A second create of the same name is the server's refusal, said, and changes nothing.
    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &[
            "create",
            "nordisk/knut",
            "--sha1",
            "--host",
            &host(&h),
            "-i",
            &key,
        ],
    );
    assert!(
        !ok,
        "a second create of nordisk/knut must be refused\n  stdout: {out}\n  stderr: {err}"
    );
    assert_eq!(
        catalog(&h, "nordisk", "knut")
            .expect("still there")
            .hash_kind,
        HashKind::Sha256
    );

    // A namespace that does not exist is the server's sentence too.
    let (ok, _, err) = gunnar(
        &h,
        h.dir.path(),
        &["create", "nowhere/x", "--host", &host(&h), "-i", &key],
    );
    assert!(!ok && err.contains("namespace nowhere not found"), "{err}");
    assert!(catalog(&h, "nowhere", "x").is_none());
}

/// ★ `gunnar settings` shows a repository, and changes its visibility and default branch.
#[test]
fn gunnar_settings_shows_and_changes_a_repository_through_the_forges_door() {
    let h = appliance();
    let key = h.key_path.to_str().expect("utf-8").to_owned();
    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &["create", "nordisk/knut", "--host", &host(&h), "-i", &key],
    );
    assert!(ok, "gunnar create\n  stdout: {out}\n  stderr: {err}");

    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &["settings", "nordisk/knut", "--host", &host(&h), "-i", &key],
    );
    assert!(
        ok,
        "gunnar settings (show)\n  stdout: {out}\n  stderr: {err}"
    );
    assert!(
        out.contains("repository nordisk/knut") && out.contains("visibility private"),
        "{out}"
    );

    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &[
            "settings",
            "nordisk/knut",
            "--visibility",
            "public",
            "--default-branch",
            "trunk",
            "--host",
            &host(&h),
            "-i",
            &key,
        ],
    );
    assert!(ok, "gunnar settings --visibility public --default-branch trunk\n  stdout: {out}\n  stderr: {err}");
    assert!(
        out.contains("visibility public") && out.contains("default-branch refs/heads/trunk"),
        "{out}"
    );
    let repo = catalog(&h, "nordisk", "knut").expect("there");
    assert_eq!(
        repo.visibility,
        Visibility::Public,
        "the catalog says public"
    );
    assert_eq!(
        repo.default_branch, "refs/heads/trunk",
        "the catalog holds the new default branch"
    );

    // Back to private, read back by a plain show.
    let (ok, _, err) = gunnar(
        &h,
        h.dir.path(),
        &[
            "settings",
            "nordisk/knut",
            "--visibility",
            "private",
            "--host",
            &host(&h),
            "-i",
            &key,
        ],
    );
    assert!(ok, "{err}");
    assert_eq!(
        catalog(&h, "nordisk", "knut").expect("there").visibility,
        Visibility::Private
    );

    // A value the verb does not know is refused before any connection.
    let (ok, _, err) = gunnar(
        &h,
        h.dir.path(),
        &[
            "settings",
            "nordisk/knut",
            "--visibility",
            "secret",
            "--host",
            &host(&h),
            "-i",
            &key,
        ],
    );
    assert!(
        !ok && err.contains("public") && err.contains("private"),
        "{err}"
    );

    // A repository that does not exist is the server's refusal, said (the door does not
    // tell a missing repository from one the caller may not read).
    let (ok, out, err) = gunnar(
        &h,
        h.dir.path(),
        &[
            "settings",
            "nordisk/nothing",
            "--host",
            &host(&h),
            "-i",
            &key,
        ],
    );
    assert!(
        !ok && err.contains("nordisk/nothing") && out.is_empty(),
        "stdout: {out}\nstderr: {err}"
    );
}

/// ★ **A sha256 tree's first `gunnar push` creates its repository as sha256** (ef4ec6f6):
/// a push-made repository is sha1 (the server announces its format first), so `gunnar
/// push` creates the remote over the door in sha256 before it pushes. The bytes come
/// back out through stock git.
#[test]
fn a_sha256_trees_first_gunnar_push_creates_the_repository_in_sha256() {
    let h = appliance();
    let key = h.key_path.to_str().expect("utf-8").to_owned();
    let work = h.dir.path().join("work-sha256");
    local_repo(&h, &work, HashKind::Sha256, "a sha256 tree\n");
    let url = h.url("nordisk", "s256");

    let (ok, out, err) = gunnar(
        &h,
        &work,
        &["push", &url, "refs/heads/main:refs/heads/main", "-i", &key],
    );
    assert!(ok, "gunnar push of a sha256 tree to a repository that does not exist\n  stdout: {out}\n  stderr: {err}");
    assert!(
        err.contains("created nordisk/s256") && err.contains("sha256"),
        "the push says it created the repository: {err}"
    );
    assert_eq!(
        catalog(&h, "nordisk", "s256")
            .expect("the push created it")
            .hash_kind,
        HashKind::Sha256
    );

    let back = h.dir.path().join("back");
    h.git(
        h.dir.path(),
        &["clone", "--quiet", &url, back.to_str().expect("utf-8")],
    )
    .expect_ok("clone the sha256 repository back");
    assert_eq!(
        std::fs::read_to_string(back.join("hello.txt")).expect("the cloned file"),
        "a sha256 tree\n"
    );
    let format = h.git(&back, &["rev-parse", "--show-object-format"]);
    format.expect_ok("rev-parse --show-object-format");
    assert_eq!(format.stdout.trim(), "sha256", "the clone is sha256");

    // The second push finds the repository and leaves it alone.
    std::fs::write(work.join("hello.txt"), "two\n").expect("edit");
    h.git(&work, &["commit", "--quiet", "-am", "two"])
        .expect_ok("commit");
    let (ok, out, err) = gunnar(
        &h,
        &work,
        &["push", &url, "refs/heads/main:refs/heads/main", "-i", &key],
    );
    assert!(
        ok,
        "the second gunnar push\n  stdout: {out}\n  stderr: {err}"
    );
    assert!(
        !err.contains("created"),
        "a repository that exists is not created again: {err}"
    );
}