gunnar-server 1.1.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
//! **The account audit logs' retention sweep** (2026-10-05): every few hours,
//! each account's log drops the days past its retention (a year by default),
//! so an account nothing happens in for a while does not keep entries the
//! promise says are gone. An append does the same for its own account first;
//! this is the rest. Never on a twin: its control store is a replica, and the
//! primary's sweep reaches it as transactions.

use std::sync::Arc;

use crate::stores::ServerStores;

/// How often the sweep runs.
pub const EVERY: std::time::Duration = std::time::Duration::from_secs(6 * 3600);

/// **One pass**: every account (a person who is not an employee), pruned at
/// `now`. Returns how many entries went. A failure is one WARN per account.
pub fn sweep_once(stores: &ServerStores, now: i64) -> u64 {
    if stores.is_twin() {
        return 0;
    }
    let auth = stores.auth();
    let Ok(principals) = auth.principals() else {
        return 0;
    };
    let mut dropped = 0;
    for p in principals {
        if p.kind != gunnar_core::PrincipalKind::User || !matches!(auth.employer_of(&p.id), Ok(None)) {
            continue;
        }
        match stores.account_audit().prune(&p.id, now) {
            Ok(n) => dropped += n,
            Err(err) => tracing::warn!(
                target: "gunnar::audit",
                event = "account_audit.sweep_failed",
                account = %p.id,
                error = %err,
                "an account's audit log could not be swept"
            ),
        }
    }
    if dropped > 0 {
        tracing::info!(
            target: "gunnar::audit",
            event = "account_audit.swept",
            dropped,
            "account audit entries past their retention were dropped"
        );
    }
    dropped
}

/// The sweep, for ever (spawned by `serve`).
pub async fn run(stores: Arc<ServerStores>) {
    let mut ticker = tokio::time::interval(EVERY);
    loop {
        ticker.tick().await;
        let stores = Arc::clone(&stores);
        let _ = tokio::task::spawn_blocking(move || sweep_once(&stores, gunnar_core::now_ms())).await;
    }
}