gunnar-grpc 1.5.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! `Refs`. Read-only access to refs, the commit log, objects and diffs.
//!
//! Every RPC here reaches through the storage seam into what will be
//! `gunnar-store`'s `RefStore` / `ObjectStore` (the storage seam). Until that lands the seam
//! answers `UNIMPLEMENTED`, which is deliberate: an empty ref list would be
//! indistinguishable from a working server with no refs (measured, not asserted).
//!
//! There is no RPC that MOVES a ref, by construction. Refs move through
//! `git-receive-pack` on the SSH listener, behind the policy gate (the pre-receive gate). A
//! `Refs.Set` would be a push that skipped every gate.

use gunnar_proto::refs_server::Refs;
use gunnar_proto::{
    BlameRequest, BlameResponse, DiffRequest, DiffResponse, GetRefRequest, GetRefResponse,
    ListRefsRequest, ListRefsResponse, LogRequest, LogResponse, ReadLfsObjectRequest,
    ReadLfsObjectResponse, ReadObjectRequest, ReadObjectResponse, SearchRequest, SearchResponse,
};
use tonic::{Request, Response, Status};

use crate::auth::Access;
use crate::service::RefsService;
use crate::status::repo_path;
use gunnar_proto::GrantLevel;

/// How many commits `Log` returns when the caller does not say.
const DEFAULT_LOG_PAGE: u32 = 100;
/// And the ceiling, so one call cannot ask the store to walk a whole history
/// into memory.
const MAX_LOG_PAGE: u32 = 1000;

#[tonic::async_trait]
impl Refs for RefsService {
    async fn list(
        &self,
        request: Request<ListRefsRequest>,
    ) -> Result<Response<ListRefsResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx
            .require(&caller, Access::repo(&path), GrantLevel::Read, "Refs.List")?;
        let refs = self
            .ctx
            .control
            .list_refs(&path, &request.prefix, request.peel)?;
        Ok(Response::new(ListRefsResponse {
            refs,
            next_page_token: String::new(),
        }))
    }

    async fn get(
        &self,
        request: Request<GetRefRequest>,
    ) -> Result<Response<GetRefResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx
            .require(&caller, Access::repo(&path), GrantLevel::Read, "Refs.Get")?;
        if request.ref_name.is_empty() {
            return Err(Status::invalid_argument("`ref_name` is required"));
        }
        let ref_target = self
            .ctx
            .control
            .get_ref(&path, &request.ref_name, request.peel)?;
        Ok(Response::new(GetRefResponse {
            ref_target: Some(ref_target),
        }))
    }

    async fn log(&self, request: Request<LogRequest>) -> Result<Response<LogResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx
            .require(&caller, Access::repo(&path), GrantLevel::Read, "Refs.Log")?;
        let limit = match request.page_size {
            0 => DEFAULT_LOG_PAGE,
            n => n.min(MAX_LOG_PAGE),
        };
        let commits = if request.all_refs {
            if !request.start.is_empty() {
                return Err(Status::invalid_argument(
                    "`all_refs` walks from every ref tip; `start` must be empty with it",
                ));
            }
            self.ctx.control.log_all_refs(&path, limit, &request.path)?
        } else {
            self.ctx
                .control
                .log(&path, &request.start, limit, &request.path)?
        };
        Ok(Response::new(LogResponse {
            commits,
            next_page_token: String::new(),
        }))
    }

    async fn read_object(
        &self,
        request: Request<ReadObjectRequest>,
    ) -> Result<Response<ReadObjectResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx.require(
            &caller,
            Access::repo(&path),
            GrantLevel::Read,
            "Refs.ReadObject",
        )?;
        if request.oid.is_empty() {
            return Err(Status::invalid_argument("`oid` is required"));
        }
        let response = self
            .ctx
            .control
            .read_object(&path, &request.oid, request.max_bytes)?;
        Ok(Response::new(response))
    }

    /// **The bytes of a file that is a pointer in the tree.**
    ///
    /// Read on the repository, like every verb here: an LFS object belongs to
    /// the repository whose tree points at it, and gunnar's LFS wires — the
    /// SSH adapter and the HTTP batch API — decide `download` on that same
    /// grant. Nothing here widens who may read what; it adds a door for a
    /// caller that holds the grant and speaks gRPC.
    async fn read_lfs_object(
        &self,
        request: Request<ReadLfsObjectRequest>,
    ) -> Result<Response<ReadLfsObjectResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx.require(
            &caller,
            Access::repo(&path),
            GrantLevel::Read,
            "Refs.ReadLfsObject",
        )?;
        if request.oid.is_empty() {
            return Err(Status::invalid_argument("`oid` is required"));
        }
        let response = self.ctx.control.read_lfs_object(
            &path,
            &request.oid,
            request.max_bytes,
            (request.offset != 0 || request.length != 0)
                .then_some((request.offset, request.length)),
        )?;
        Ok(Response::new(response))
    }

    async fn diff(&self, request: Request<DiffRequest>) -> Result<Response<DiffResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx
            .require(&caller, Access::repo(&path), GrantLevel::Read, "Refs.Diff")?;
        let changes =
            self.ctx
                .control
                .diff(&path, &request.from, &request.to, &request.path_prefixes)?;
        Ok(Response::new(DiffResponse { changes }))
    }

    async fn blame(
        &self,
        request: Request<BlameRequest>,
    ) -> Result<Response<BlameResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx
            .require(&caller, Access::repo(&path), GrantLevel::Read, "Refs.Blame")?;
        if request.path.is_empty() {
            return Err(Status::invalid_argument("`path` is required"));
        }
        let response = self.ctx.control.blame(&path, &request.rev, &request.path)?;
        Ok(Response::new(response))
    }

    async fn search(
        &self,
        request: Request<SearchRequest>,
    ) -> Result<Response<SearchResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let path = repo_path(request.repo, "repo")?;
        self.ctx.require(
            &caller,
            Access::repo(&path),
            GrantLevel::Read,
            "Refs.Search",
        )?;
        if request.pattern.is_empty() {
            return Err(Status::invalid_argument("`pattern` is required"));
        }
        let response = self.ctx.control.search(
            &path,
            &request.rev,
            &request.pattern,
            request.max_file_bytes,
            request.max_matches,
        )?;
        Ok(Response::new(response))
    }
}