Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-forge/src/repo.rs

//! **The one shape every forge is flattened to**, and the reason it has exactly
//! these fields and no more.
//!
//! A picker shows a list and a mirror consumes the choice. Everything here is
//! needed by one of those two and nothing here is forge-specific, which is the
//! property that lets `gunnar-cli` and the UI's BFF render the same list
//! without either of them learning what a GitLab "project" is.

use std::fmt;

/// Which forge a [`Repo`] came from.
///
/// Deliberately an enum and not a `String`: it is carried in every error
/// (`ForgeError` names the forge and nothing else about the request), and an
/// error message assembled from a caller-supplied string is one refactor away
/// from being assembled from a caller-supplied *token*.
///
/// `Codeberg` names the Gitea/Forgejo API family rather than the host — a
/// self-hosted Forgejo at `git.example.com` answers the same v1 API and
/// reports itself as `Codeberg` here. The alternative, a `SelfHosted(String)`
/// variant, would put a hostname into every error and every log line for no
/// gain: the base URL is the caller's own input, so the caller already knows
/// which host it asked.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ForgeKind {
    /// `api.github.com`, or a GitHub Enterprise Server at `<host>/api/v3`.
    GitHub,
    /// Codeberg, and every other Forgejo/Gitea at `<host>/api/v1`.
    Codeberg,
    /// `gitlab.com`, or a self-managed GitLab at `<host>/api/v4`.
    GitLab,
}

impl ForgeKind {
    /// The lowercase name used in errors, logs and the picker's column.
    pub fn as_str(self) -> &'static str {
        match self {
            ForgeKind::GitHub => "github",
            ForgeKind::Codeberg => "codeberg",
            ForgeKind::GitLab => "gitlab",
        }
    }
}

impl fmt::Display for ForgeKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// One repository, as a picker needs to show it and a mirror needs to clone it.
///
/// # Why BOTH clone URLs
///
/// gunnar mirrors over two credentials and the choice is made *after* the
/// list: a deploy key wants `clone_url_ssh`, a personal access token wants
/// `clone_url_https`. Keeping only one would mean re-deriving the other by
/// string surgery on a host we do not control — `git@codeberg.org:u/r.git`
/// versus `ssh://git@git.example.com:2222/u/r.git` are both correct and no
/// rule turns one into the other. The forges hand us both; we keep both.
///
/// # Why `size_kib` is an `Option`
///
/// GitHub and Gitea/Forgejo report `size` in KiB on every repository object.
/// GitLab reports nothing at all unless the request asked for `statistics=true`
/// AND the caller is an administrator, in which case it is
/// `statistics.repository_size` in BYTES. A `u64` with a `0` default would
/// make every GitLab repository look empty in a picker sorted by size, so the
/// absence is spelled out instead of guessed at.
///
/// # No `Debug` hazard
///
/// Nothing here is a credential, so `Debug` is derived. That is safe *because*
/// of where the token lives: it is a `&str` argument to one function and is
/// never put in a struct — see `crate::error` for the whole argument.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Repo {
    /// Which forge answered.
    pub forge: ForgeKind,
    /// `owner/name`. GitLab's `path_with_namespace` can be deeper than two
    /// segments (`group/subgroup/project`) and is kept whole — truncating it
    /// would produce a name that does not resolve.
    pub full_name: String,
    /// The `https://` clone URL, for the token path.
    pub clone_url_https: String,
    /// The SSH clone URL, for the deploy-key path.
    pub clone_url_ssh: String,
    /// The branch a fresh clone checks out. Empty on a repository with no
    /// commits, which every forge reports as `null` or `""`.
    pub default_branch: String,
    /// GitLab's `internal` visibility counts as private here: it is not
    /// world-readable, which is the only distinction a mirror cares about.
    pub private: bool,
    /// Archived/read-only upstream. A picker greys these; a mirror still
    /// mirrors them, which is often exactly why someone wants one.
    pub archived: bool,
    /// Size in KiB, when the forge says. See the type-level note.
    pub size_kib: Option<u64>,
    /// The forge's description, absent rather than empty when unset.
    pub description: Option<String>,
}