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

nordisk/gunnar

crates/gunnar-server/tests/log_all_refs.rs

//! **`Refs.Log` from every ref tip, and from a named branch.**
//!
//! The History page walked one start (the default branch), so a commit that
//! lives only on a side branch never showed, while its ref chip did. Owner
//! order 2026-10-18: History gets a branch picker and an "all branches" view,
//! and the ref walk is the server's job. This drives `CoreControlPlane` on a
//! real store with three refs and asserts the EXACT oids each walk returns:
//!
//! ```text
//!   S (t=3000)  refs/heads/side      — only on side
//!   M (t=2000)  refs/heads/main, HEAD
//!   T (t=1500)  refs/tags/v1         — only on the tag
//!   R (t=1000)  the root, shared
//! ```

use std::sync::Arc;

use gix_object::Kind;
use gunnar_core::{HashKind, NewRepo, RepoRef};
use gunnar_grpc::ControlPlane as _;
use gunnar_proto::RepoPath;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use gunnar_store::conformance::pack::PackBuilder;
use gunnar_store::git_storage_trait::GitServe;

type Oid = gix_hash::ObjectId;

fn compute(hash: gix_hash::Kind, kind: Kind, content: &[u8]) -> Oid {
    gix_object::compute_hash(hash, kind, content).expect("hashing an object")
}

fn commit_bytes(tree: Oid, parents: &[Oid], when: i64, message: &str) -> Vec<u8> {
    let mut body = format!("tree {}\n", tree.to_hex());
    for parent in parents {
        body.push_str(&format!("parent {}\n", parent.to_hex()));
    }
    let who = format!("Alice <alice@example.com> {when} +0000");
    body.push_str(&format!("author {who}\n"));
    body.push_str(&format!("committer {who}\n\n"));
    body.push_str(message);
    body.push('\n');
    body.into_bytes()
}

struct Planted {
    root: Oid,
    main: Oid,
    side: Oid,
    tag: Oid,
}

fn plant(git: &dyn GitServe, hash: gix_hash::Kind) -> Planted {
    // One empty tree serves every commit: the walk is about topology.
    let tree = Vec::new();
    let tree_id = compute(hash, Kind::Tree, &tree);
    let r = commit_bytes(tree_id, &[], 1000, "root");
    let root = compute(hash, Kind::Commit, &r);
    let m = commit_bytes(tree_id, &[root], 2000, "on main");
    let main = compute(hash, Kind::Commit, &m);
    let s = commit_bytes(tree_id, &[root], 3000, "only on side");
    let side = compute(hash, Kind::Commit, &s);
    let t = commit_bytes(tree_id, &[root], 1500, "only on the tag");
    let tag = compute(hash, Kind::Commit, &t);

    let mut pb = PackBuilder::new(hash);
    pb.object(Kind::Tree, tree);
    for body in [r, m, s, t] {
        pb.object(Kind::Commit, body);
    }
    let fixture = pb.build().expect("building the pack");
    git.put_pack(&fixture.bytes).expect("storing the pack");
    git.update_ref("refs/heads/main", None, Some(main.as_bytes()))
        .expect("main");
    git.update_ref("refs/heads/side", None, Some(side.as_bytes()))
        .expect("side");
    git.update_ref("refs/tags/v1", None, Some(tag.as_bytes()))
        .expect("v1");
    git.set_head("refs/heads/main").expect("HEAD → main");
    Planted {
        root,
        main,
        side,
        tag,
    }
}

fn oids(commits: &[gunnar_proto::Commit]) -> Vec<String> {
    commits.iter().map(|c| c.oid.clone()).collect()
}

fn world() -> (tempfile::TempDir, CoreControlPlane, RepoPath, Planted) {
    let dir = tempfile::tempdir().unwrap();
    let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
    stores
        .catalog()
        .create_namespace("alice")
        .expect("namespace");
    let repo_ref = RepoRef::new("alice", "branches").expect("legal repo path");
    let repo = stores
        .create_repo(NewRepo::new(repo_ref).hash_kind(HashKind::Sha256), false)
        .expect("create repo");
    let store = stores.repo_store(&repo).expect("open repo store");
    let planted = plant(store.serve(), store.hash_kind());
    drop(store);
    let plane = CoreControlPlane::new(Arc::clone(&stores));
    (dir, plane, RepoPath::new("alice", "branches"), planted)
}

#[test]
fn the_default_walk_misses_the_side_branch_and_all_refs_finds_it() {
    let (_dir, plane, path, p) = world();

    // Today's walk: the default branch only.
    let default = plane.log(&path, "", 100, "").expect("default log");
    assert_eq!(
        oids(&default),
        vec![p.main.to_string(), p.root.to_string()],
        "an empty start walks HEAD (main) only"
    );

    // Every ref tip at once, newest committer time first, the shared root once.
    let all = plane.log_all_refs(&path, 100, "").expect("all-refs log");
    assert_eq!(
        oids(&all),
        vec![
            p.side.to_string(),
            p.main.to_string(),
            p.tag.to_string(),
            p.root.to_string()
        ],
        "all_refs must show the side-branch and tag-only commits, root once"
    );

    // Paged like a single-start walk: the limit caps the page.
    let page = plane.log_all_refs(&path, 2, "").expect("a short page");
    assert_eq!(oids(&page), vec![p.side.to_string(), p.main.to_string()]);
}

#[test]
fn a_named_branch_starts_the_walk_at_that_branch() {
    let (_dir, plane, path, p) = world();
    let side = plane
        .log(&path, "refs/heads/side", 100, "")
        .expect("side log");
    assert_eq!(oids(&side), vec![p.side.to_string(), p.root.to_string()]);
}