gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **A repository's settings, asked over another door** (owner, 2026-10-14).
//!
//! On an installed estate the control plane is bound to the WireGuard overlay,
//! so a person's own machine cannot reach `Repos.Get` / `Repos.SetVisibility`
//! / `Repos.SetDefaultBranch`. The forge's SSH door can: it proves who asks
//! with the person's key and hands the ask here. This module decides NOTHING:
//! it builds the same `tonic::Request` the gRPC door would, with the
//! [`Caller`] the SSH door authenticated attached the way [`crate::auth`]'s
//! gate attaches it, and calls the SAME [`ReposService`] handlers — so the
//! account-admin rule for visibility, the repository-Admin rule for the
//! default branch, the account audit event and the browse replica's notice
//! are the handlers' own, not a copy.

use gunnar_proto::repos_server::Repos;
use gunnar_proto::{CreateRepoRequest, GetRepoRequest, HashKind, Repo, RepoPath, SetDefaultBranchRequest, SetVisibilityRequest, Visibility};
use tonic::{Request, Status};

use crate::auth::Caller;
use crate::service::{ReposService, ServiceContext};

/// What is asked of one repository.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RepoSetting {
    /// `Repos.Get`.
    Show,
    /// `Repos.SetVisibility`: `true` is public.
    Visibility(bool),
    /// `Repos.SetDefaultBranch`, the full ref name (`refs/heads/…`).
    DefaultBranch(String),
    /// `Repos.Create` of an empty repository: `true` is sha256.
    Create(bool),
}

/// Ask `Repos` as `principal` (authenticated by `fingerprint`'s key).
pub async fn ask(
    ctx: &ServiceContext,
    principal: &str,
    fingerprint: &str,
    namespace: &str,
    repo: &str,
    setting: RepoSetting,
) -> Result<Repo, Status> {
    let caller = Caller {
        principal_id: principal.to_owned(),
        fingerprint: fingerprint.to_owned(),
        // A person's key over the forge's door. The browse replica's single
        // machine write never comes this way.
        machine: false,
        authenticated: true,
    };
    fn as_caller<T>(caller: &Caller, message: T) -> Request<T> {
        let mut request = Request::new(message);
        request.extensions_mut().insert(caller.clone());
        request
    }
    let path = Some(RepoPath { namespace: namespace.to_owned(), repo: repo.to_owned() });
    let service = ReposService::new(ctx.clone());
    let repo = match setting {
        RepoSetting::Show => service.get(as_caller(&caller, GetRepoRequest { path })).await?.into_inner().repo,
        RepoSetting::Visibility(public) => {
            let visibility = if public { Visibility::Public } else { Visibility::Private } as i32;
            service
                .set_visibility(as_caller(&caller, SetVisibilityRequest { path, visibility }))
                .await?
                .into_inner()
                .repo
        }
        RepoSetting::Create(sha256) => {
            let hash_kind = if sha256 { HashKind::Sha256 } else { HashKind::Sha1 } as i32;
            service
                .create(as_caller(&caller, CreateRepoRequest { path, hash_kind, ..Default::default() }))
                .await?
                .into_inner()
                .repo
        }
        RepoSetting::DefaultBranch(default_branch) => {
            service
                .set_default_branch(as_caller(&caller, SetDefaultBranchRequest { path, default_branch }))
                .await?
                .into_inner()
                .repo
        }
    };
    repo.ok_or_else(|| Status::internal("the repository service answered with no repository"))
}

/// The answer as `key value` lines, the shape the SSH verb prints.
pub fn render(repo: &Repo) -> String {
    let path = repo.path.as_ref().map(|p| format!("{}/{}", p.namespace, p.repo)).unwrap_or_default();
    let visibility = if repo.visibility == Visibility::Public as i32 { "public" } else { "private" };
    let format = if repo.hash_kind == HashKind::Sha256 as i32 { "sha256" } else { "sha1" };
    format!(
        "repository {path}\nvisibility {visibility}\ndefault-branch {}\nencrypted {}\nobject-format {format}\n",
        repo.default_branch,
        if repo.encrypted { "yes" } else { "no" }
    )
}