gunnar-server 2.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **A tiny hand-built git, for tests that must know the answer in advance.**
//!
//! `blame` and `search` are only worth trusting if a test can assert the *exact*
//! commit a line came from or the *exact* file:line a string sits at — not
//! merely that some result came back (measured, not asserted). That needs a repository whose
//! commit graph the test authored: this builds blobs, trees and commits into a
//! real [`MemGitServe`], the same in-memory backend the store conformance suite
//! runs against, so the oids are real git object ids and `blame`/`search` walk
//! them through exactly the readers the appliance uses.
//!
//! Each object is stored as its own one-object pack. [`MemGitServe`] indexes a
//! pack on `put_pack` and never checks connectivity *across* packs, so a tree
//! may name a blob a prior pack carried — which is what lets these helpers build
//! a graph one object at a time.

use gunnar_store::conformance::pack::PackBuilder;
use gunnar_store::git_storage_trait::{GitOps, GitServe};
use gunnar_store::mem::MemGitServe;
use gix_hash::ObjectId;
use gix_object::Kind;

/// The tree mode of a regular non-executable file, as git writes it.
pub const TREE_MODE_BLOB: &str = "100644";
/// The tree mode of a subtree.
pub const TREE_MODE_TREE: &str = "40000";

/// A repository the test wrote, object by object.
pub struct Repo {
    git: MemGitServe,
    hash: gix_hash::Kind,
}

impl Repo {
    /// A fresh, empty sha256 repository.
    pub fn new() -> Self {
        let hash = gix_hash::Kind::Sha256;
        Repo {
            git: MemGitServe::new(hash),
            hash,
        }
    }

    /// The backend, as the trait object `blame_file` and `search_tree` take.
    pub fn serve(&self) -> &dyn GitServe {
        &self.git
    }

    /// The hash kind, for a caller that must pass it alongside [`Repo::serve`].
    pub fn hash_kind(&self) -> gix_hash::Kind {
        self.hash
    }

    /// Store one object and return the id git would give it.
    fn put(&self, kind: Kind, content: Vec<u8>) -> ObjectId {
        let id = gix_object::compute_hash(self.hash, kind, &content).expect("hashing an object");
        let mut builder = PackBuilder::new(self.hash);
        builder.object(kind, content);
        let fixture = builder.build().expect("building a one-object pack");
        self.git.put_pack(&fixture.bytes).expect("storing the pack");
        id
    }

    /// A blob holding `content`.
    pub fn blob(&self, content: &[u8]) -> ObjectId {
        self.put(Kind::Blob, content.to_vec())
    }

    /// A tree of `(mode, name, oid)` entries.
    ///
    /// Entries are sorted by name, as git writes them; the readers do not depend
    /// on the order, but a canonical tree keeps the oids the ones stock git
    /// would compute.
    pub fn tree(&self, entries: &[(&str, &str, ObjectId)]) -> ObjectId {
        let mut sorted = entries.to_vec();
        sorted.sort_by(|a, b| a.1.as_bytes().cmp(b.1.as_bytes()));
        let mut body = Vec::new();
        for (mode, name, oid) in sorted {
            body.extend_from_slice(mode.as_bytes());
            body.push(b' ');
            body.extend_from_slice(name.as_bytes());
            body.push(0);
            body.extend_from_slice(oid.as_bytes());
        }
        self.put(Kind::Tree, body)
    }

    /// A commit of `tree` with `parents`, authored (and committed) by `author`.
    pub fn commit(
        &self,
        tree: ObjectId,
        parents: &[ObjectId],
        author: &str,
        message: &str,
    ) -> ObjectId {
        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!("{author} <{author}@example.com> 1700000000 +0000");
        body.push_str(&format!("author {who}\n"));
        body.push_str(&format!("committer {who}\n\n"));
        body.push_str(message);
        body.push('\n');
        self.put(Kind::Commit, body.into_bytes())
    }
}