pub mod load; use std::{ collections::HashMap, fmt::{Display, Formatter}, ops::Deref, path::PathBuf, }; use serde::Deserialize; use terrors::OneOf; use crate::filesystem::FileSystem; /// Mapped from the `git-next-server.toml` file #[derive(Debug, PartialEq, Eq, Deserialize)] pub struct ServerConfig { webhook: Webhook, storage: ServerStorage, forge: HashMap, } impl ServerConfig { pub(crate) fn load(fs: &FileSystem) -> Result> { let str = fs.read_file("git-next-server.toml").map_err(OneOf::new)?; toml::from_str(&str).map_err(OneOf::new) } pub(crate) fn forges(&self) -> impl Iterator { self.forge .iter() .map(|(name, forge)| (ForgeName(name.clone()), forge)) } pub const fn storage(&self) -> &ServerStorage { &self.storage } pub const fn webhook(&self) -> &Webhook { &self.webhook } } /// Defines the Webhook Forges should send updates to #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct Webhook { url: String, } impl Webhook { pub fn url(&self) -> WebhookUrl { WebhookUrl(self.url.clone()) } } /// The URL for the webhook where forges should send their updates #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct WebhookUrl(String); impl AsRef for WebhookUrl { fn as_ref(&self) -> &str { &self.0 } } /// The directory to store server data, such as cloned repos #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct ServerStorage { path: PathBuf, } /// Mapped from `.git-next.toml` file in target repo /// Is also derived from the optional parameters in `git-next-server.toml` at /// `forge.{forge}.repos.{repo}.(main|next|dev)` #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct RepoConfig { branches: RepoBranches, } impl RepoConfig { #[cfg(test)] pub const fn new(branches: RepoBranches) -> Self { Self { branches } } // #[cfg(test)] pub fn load(toml: &str) -> Result { toml::from_str(toml) } pub const fn branches(&self) -> &RepoBranches { &self.branches } } impl Display for RepoConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.branches) } } /// Mapped from `.git-next.toml` file at `branches` #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct RepoBranches { main: String, next: String, dev: String, } impl RepoBranches { #[cfg(test)] pub fn new(main: impl Into, next: impl Into, dev: impl Into) -> Self { Self { main: main.into(), next: next.into(), dev: dev.into(), } } pub fn main(&self) -> BranchName { BranchName(self.main.clone()) } pub fn next(&self) -> BranchName { BranchName(self.next.clone()) } pub fn dev(&self) -> BranchName { BranchName(self.dev.clone()) } } impl Display for RepoBranches { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } /// Defines a Forge to connect to /// Maps from `git-next-server.toml` at `forge.{forge}` #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct ForgeConfig { forge_type: ForgeType, hostname: String, user: String, token: String, // API Token // Private SSH Key Path repos: HashMap, } impl ForgeConfig { #[allow(dead_code)] pub const fn forge_type(&self) -> &ForgeType { &self.forge_type } pub fn hostname(&self) -> Hostname { Hostname(self.hostname.clone()) } pub fn user(&self) -> User { User(self.user.clone()) } pub fn token(&self) -> ApiToken { ApiToken(self.token.clone().into()) } pub fn repos(&self) -> impl Iterator { self.repos .iter() .map(|(name, repo)| (RepoAlias(name.clone()), repo)) } } impl Display for ForgeConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{} - {}@{}", self.forge_type, self.user, self.hostname) } } /// Defines a Repo within a ForgeConfig to be monitored by the server /// Maps from `git-next-server.toml` at `forge.{forge}.repos.{name}` #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct ServerRepoConfig { repo: String, branch: String, gitdir: Option, main: Option, next: Option, dev: Option, } impl ServerRepoConfig { #[allow(dead_code)] pub fn repo(&self) -> RepoPath { RepoPath(self.repo.clone()) } #[allow(dead_code)] pub fn branch(&self) -> BranchName { BranchName(self.branch.clone()) } /// Returns a RepoConfig from the server configuration if ALL THREE branches were provided pub fn repo_config(&self) -> Option { match (&self.main, &self.next, &self.dev) { (Some(main), Some(next), Some(dev)) => Some(RepoConfig { branches: RepoBranches { main: main.to_string(), next: next.to_string(), dev: dev.to_string(), }, }), _ => None, } } } #[cfg(test)] impl AsRef for ServerRepoConfig { fn as_ref(&self) -> &Self { self } } impl Display for ServerRepoConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{} - {}", self.repo, self.branch) } } /// The name of a Forge to connect to #[derive(Clone, Debug, PartialEq, Eq)] pub struct ForgeName(pub String); impl Display for ForgeName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// The hostname of a forge #[derive(Clone, Debug, PartialEq, Eq)] pub struct Hostname(pub String); impl Display for Hostname { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// The user within the forge to connect as #[derive(Clone, Debug, PartialEq, Eq)] pub struct User(pub String); impl Display for User { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// The API Token for the [user] /// ForgeJo: https://{hostname}/user/settings/applications /// Github: https://github.com/settings/tokens #[derive(Clone, Debug)] pub struct ApiToken(pub secrecy::Secret); impl From for ApiToken { fn from(value: String) -> Self { Self(value.into()) } } /// The API Token is in effect a password, so it must be explicitly exposed to access its value impl secrecy::ExposeSecret for ApiToken { fn expose_secret(&self) -> &String { self.0.expose_secret() } } /// The derived information about a Forge, used to create interactions with it #[derive(Clone, Debug)] pub struct ForgeDetails { pub forge_name: ForgeName, pub forge_type: ForgeType, pub hostname: Hostname, pub user: User, pub token: ApiToken, // API Token // Private SSH Key Path } impl From<(&ForgeName, &ForgeConfig)> for ForgeDetails { fn from(forge: (&ForgeName, &ForgeConfig)) -> Self { Self { forge_name: forge.0.clone(), forge_type: forge.1.forge_type.clone(), hostname: forge.1.hostname(), user: forge.1.user(), token: forge.1.token(), } } } /// The alias of a repo /// This is the alias for the repo within `git-next-server.toml` #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RepoAlias(pub String); impl Display for RepoAlias { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// The path for the repo within the forge. /// Typically this is composed of the user or organisation and the name of the repo /// e.g. `{user}/{repo}` #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoPath(pub String); impl Display for RepoPath { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// The name of a Branch #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct BranchName(pub String); impl Display for BranchName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Deref for BranchName { type Target = String; fn deref(&self) -> &Self::Target { &self.0 } } /// The derived information about a repo, used to interact with it #[derive(Clone, Debug)] pub struct RepoDetails { pub repo_alias: RepoAlias, pub repo_path: RepoPath, pub branch: BranchName, pub forge: ForgeDetails, pub repo_config: Option, pub gitdir: GitDir, } impl RepoDetails { pub fn new( name: &RepoAlias, server_repo_config: &ServerRepoConfig, forge_name: &ForgeName, forge_config: &ForgeConfig, server_storage: &ServerStorage, ) -> Self { Self { repo_alias: name.clone(), repo_path: RepoPath(server_repo_config.repo.clone()), repo_config: server_repo_config.repo_config(), branch: BranchName(server_repo_config.branch.clone()), gitdir: GitDir( server_storage .path .join(forge_name.to_string()) .join(name.to_string()), ), forge: ForgeDetails { forge_name: forge_name.clone(), forge_type: forge_config.forge_type.clone(), hostname: forge_config.hostname(), user: forge_config.user(), token: forge_config.token(), }, } } pub fn origin(&self) -> secrecy::Secret { let repo_details = self; let user = &repo_details.forge.user; let hostname = &repo_details.forge.hostname; let repo_path = &repo_details.repo_path; use secrecy::ExposeSecret; let expose_secret = &repo_details.forge.token; let token = expose_secret.expose_secret(); let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git"); origin.into() } } impl Display for RepoDetails { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}/{} ({}): {}:{}/{} @ {}", self.forge.forge_name, self.repo_alias, self.forge.forge_type, self.forge.hostname, self.forge.user, self.repo_path, self.branch, ) } } /// Identifier for the type of Forge #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub enum ForgeType { #[cfg(feature = "forgejo")] ForgeJo, // Gitea, // GitHub, // GitLab, // BitBucket, #[cfg(test)] MockForge, } impl Display for ForgeType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct GitDir(PathBuf); impl GitDir { #[cfg(test)] pub(crate) fn new(pathbuf: &std::path::Path) -> Self { Self(pathbuf.to_path_buf()) } #[allow(dead_code)] // TODO: pub const fn pathbuf(&self) -> &PathBuf { &self.0 } } impl std::fmt::Display for GitDir { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.0.display()) } } impl From<&str> for GitDir { fn from(value: &str) -> Self { Self(value.into()) } } #[cfg(test)] mod tests;