wsb 0.227.4750

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
//! The workbench's way to the machine's GitHub mirror.
//!
//! Everything GitHub is `wsb_github`: the client, the mirror in `$PRJ/.state`,
//! the schedule that keeps it current, and every read and write. What is here
//! is only what a web surface adds — how a refusal becomes a status code, and
//! which repositories this workspace has.

use std::sync::Arc;

use axum::http::StatusCode;
use axum::Json;

use super::handlers::sessions::ErrorResponse;
use super::state::WebState;

/// The machine's GitHub, as this process reaches it.
pub struct GitHub {
    pub prj: wsb_prj::Prj,
    pub service: wsb_github::Service,
}

impl GitHub {
    /// The mirror of the `$PRJ` this process was started under, or why there
    /// is none — `PRJ` unset, or a mirror that cannot be opened. Not a reason
    /// to refuse to serve the rest of the workbench: every GitHub route says
    /// it instead.
    pub fn open() -> Result<Arc<Self>, String> {
        let prj = wsb_prj::Prj::from_env().map_err(|e| e.to_string())?;
        let mirror = wsb_github::Mirror::of(&prj).map_err(|e| format!("{e:#}"))?;
        Ok(Arc::new(Self {
            prj,
            service: wsb_github::Service::new(mirror),
        }))
    }
}

pub type Refusal = (StatusCode, Json<ErrorResponse>);

fn refusal(status: StatusCode, error: impl Into<String>) -> Refusal {
    (status, Json(ErrorResponse { error: error.into() }))
}

/// This process's GitHub, or the refusal that says why there is none.
pub fn of(state: &WebState) -> Result<&GitHub, Refusal> {
    state
        .github
        .as_ref()
        .map(|github| github.as_ref())
        .map_err(|reason| refusal(StatusCode::SERVICE_UNAVAILABLE, format!("GitHub is unavailable: {reason}")))
}

/// A client with the machine's token, or the refusal that says how to log in.
pub fn client() -> Result<wsb_github::GitHubClient, Refusal> {
    wsb_github::token::resolve()
        .map(|token| wsb_github::GitHubClient::new(token.value()))
        .map_err(|no_token| refusal(StatusCode::UNAUTHORIZED, no_token.to_string()))
}

/// What a failed operation answers with.
pub fn failure(failure: wsb_github::Failure) -> Refusal {
    let status = match &failure {
        wsb_github::Failure::Invalid(_) => StatusCode::BAD_REQUEST,
        wsb_github::Failure::NoToken(_) => StatusCode::UNAUTHORIZED,
        other if other.is_authorization() => StatusCode::UNAUTHORIZED,
        wsb_github::Failure::GitHub(_) => StatusCode::BAD_GATEWAY,
        wsb_github::Failure::Mirror(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    let message = match &failure {
        // Its own words say how to log in.
        wsb_github::Failure::NoToken(message) => message.clone(),
        _ if status == StatusCode::UNAUTHORIZED => {
            "GitHub refused the token: it is invalid or lacks the access this needs".to_string()
        }
        other => other.to_string(),
    };
    refusal(status, message)
}

/// The GitHub repositories of this workspace's checkout, `owner/repo`.
pub fn repositories(state: &WebState) -> Result<Vec<wsb_github::checkout::Repository>, Refusal> {
    wsb_github::checkout::repositories(&state.workspace_root)
        .map_err(|e| refusal(StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#}")))
}

/// Keep the machine's mirror current for as long as this process serves.
///
/// The same pass `wsb github sync` runs, on the same schedule, so whichever
/// process happens to be running keeps the mirror as current as any other.
pub fn spawn_keeper(github: Arc<GitHub>) {
    tokio::spawn(async move {
        let prj = github.prj.clone();
        let mirror = github.service.mirror().clone();
        wsb_github::sync::keep_current(prj, mirror, |report| {
            for (scope, error) in &report.failed {
                tracing::debug!(%scope, %error, "a GitHub scope could not be refreshed");
            }
            for problem in &report.problems {
                tracing::debug!(%problem, "what to mirror could not be found out");
            }
        })
        .await;
    });
}