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>,
}