git-next/src/server/config/mod.rs

524 lines
15 KiB
Rust
Raw Normal View History

pub mod load;
use std::{
collections::HashMap,
fmt::{Display, Formatter},
ops::Deref,
path::PathBuf,
};
use secrecy::ExposeSecret;
2024-04-07 13:47:39 +01:00
use serde::Deserialize;
use terrors::OneOf;
use crate::filesystem::FileSystem;
2024-04-13 11:31:00 +01:00
/// Mapped from the `git-next-server.toml` file
2024-04-07 16:09:16 +01:00
#[derive(Debug, PartialEq, Eq, Deserialize)]
pub struct ServerConfig {
webhook: Webhook,
storage: ServerStorage,
forge: HashMap<String, ForgeConfig>,
}
impl ServerConfig {
pub(crate) fn load(fs: &FileSystem) -> Result<Self, OneOf<(std::io::Error, toml::de::Error)>> {
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<Item = (ForgeName, &ForgeConfig)> {
self.forge
.iter()
.map(|(name, forge)| (ForgeName(name.clone()), forge))
}
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<str> 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,
}
2024-04-13 11:31:00 +01:00
/// 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)]
2024-04-08 09:54:20 +01:00
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<Self, toml::de::Error> {
toml::from_str(toml)
2024-04-08 09:54:20 +01:00
}
pub const fn branches(&self) -> &RepoBranches {
&self.branches
}
2024-04-08 09:54:20 +01:00
}
2024-04-09 10:44:01 +01:00
impl Display for RepoConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.branches)
}
}
2024-04-13 11:31:00 +01:00
/// Mapped from `.git-next.toml` file at `branches`
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
2024-04-08 09:54:20 +01:00
pub struct RepoBranches {
main: String,
next: String,
dev: String,
}
impl RepoBranches {
#[cfg(test)]
pub fn new(main: impl Into<String>, next: impl Into<String>, dev: impl Into<String>) -> 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())
}
}
2024-04-09 10:44:01 +01:00
impl Display for RepoBranches {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
2024-04-08 09:54:20 +01:00
2024-04-13 11:31:00 +01:00
/// 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,
2024-04-07 13:47:39 +01:00
user: String,
token: String,
2024-04-07 13:47:39 +01:00
// API Token
// Private SSH Key Path
repos: HashMap<String, ServerRepoConfig>,
}
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<Item = (RepoAlias, &ServerRepoConfig)> {
self.repos
.iter()
.map(|(name, repo)| (RepoAlias(name.clone()), repo))
}
2024-04-07 13:47:39 +01:00
}
impl Display for ForgeConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} - {}@{}", self.forge_type, self.user, self.hostname)
}
}
2024-04-07 13:47:39 +01:00
/// Defines a Repo within a ForgeConfig to be monitored by the server
2024-04-13 11:31:00 +01:00
/// 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<PathBuf>,
main: Option<String>,
next: Option<String>,
dev: Option<String>,
}
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<RepoConfig> {
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<Self> for ServerRepoConfig {
2024-04-08 09:54:20 +01:00
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)
}
}
2024-04-13 11:31:00 +01:00
/// The name of a Forge to connect to
2024-04-08 08:22:23 +01:00
#[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)
}
}
2024-04-13 11:31:00 +01:00
/// The hostname of a forge
2024-04-08 08:22:23 +01:00
#[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)
}
}
2024-04-13 11:31:00 +01:00
/// The user within the forge to connect as
2024-04-08 08:22:23 +01:00
#[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)
}
}
2024-04-13 11:31:00 +01:00
/// 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<String>);
impl From<String> for ApiToken {
fn from(value: String) -> Self {
Self(value.into())
2024-04-08 11:48:35 +01:00
}
}
2024-04-13 11:31:00 +01:00
/// The API Token is in effect a password, so it must be explicitly exposed to access its value
impl ExposeSecret<String> for ApiToken {
fn expose_secret(&self) -> &String {
self.0.expose_secret()
}
}
2024-04-13 11:31:00 +01:00
/// The derived information about a Forge, used to create interactions with it
#[derive(Clone, Debug)]
pub struct ForgeDetails {
pub 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 {
name: forge.0.clone(),
forge_type: forge.1.forge_type.clone(),
hostname: forge.1.hostname(),
user: forge.1.user(),
token: forge.1.token(),
}
}
}
2024-04-13 11:31:00 +01:00
/// The alias of a repo
2024-04-13 11:31:00 +01:00
/// 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)
}
}
2024-04-13 11:31:00 +01:00
/// 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}`
2024-04-08 08:22:23 +01:00
#[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)
}
}
2024-04-13 11:31:00 +01:00
/// 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
}
}
2024-04-13 11:31:00 +01:00
/// The derived information about a repo, used to interact with it
#[derive(Clone, Debug)]
pub struct RepoDetails {
pub name: RepoAlias,
pub repo: RepoPath,
pub branch: BranchName,
pub forge: ForgeDetails,
pub config: Option<RepoConfig>,
}
impl RepoDetails {
pub fn new(
name: &RepoAlias,
repo: &ServerRepoConfig,
forge_name: &ForgeName,
forge: &ForgeConfig,
) -> Self {
Self {
name: name.clone(),
repo: RepoPath(repo.repo.clone()),
config: repo.repo_config(),
branch: BranchName(repo.branch.clone()),
forge: ForgeDetails {
name: forge_name.clone(),
forge_type: forge.forge_type.clone(),
hostname: forge.hostname(),
user: forge.user(),
token: forge.token(),
},
}
}
}
impl Display for RepoDetails {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{} ({}): {}:{}/{} @ {}",
self.forge.name,
self.name,
self.forge.forge_type,
self.forge.hostname,
self.forge.user,
self.repo,
self.branch,
)
}
}
2024-04-13 11:31:00 +01:00
/// Identifier for the type of Forge
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
2024-04-07 16:09:16 +01:00
pub enum ForgeType {
#[cfg(feature = "forgejo")]
2024-04-07 13:47:39 +01:00
ForgeJo,
// Gitea,
// GitHub,
// GitLab,
// BitBucket,
#[cfg(test)]
MockForge,
2024-04-07 13:47:39 +01:00
}
impl Display for ForgeType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[cfg(feature = "forgejo")]
2024-04-07 13:47:39 +01:00
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
2024-04-07 13:47:39 +01:00
use crate::filesystem::FileSystem;
use super::*;
#[test]
fn load_should_parse_server_config() -> Result<(), OneOf<(std::io::Error, toml::de::Error)>> {
2024-04-07 13:47:39 +01:00
let fs = FileSystem::new_temp().map_err(OneOf::new)?;
fs.write_file(
"git-next-server.toml",
r#"
[webhook]
url = "http://localhost:9909/webhook"
[storage]
path = "/opt/git-next/data"
[forge.default]
forge_type = "MockForge"
hostname = "git.example.net"
2024-04-07 13:47:39 +01:00
user = "Bob"
token = "API-Token"
[forge.default.repos]
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/user/hello.git" }
world = { repo = "user/world", branch = "master", main = "main", next = "next", dev = "dev" }
[forge.default.repos.sam]
repo = "user/sam"
branch = "main"
main = "master"
next = "upcoming"
dev = "sam-dev"
2024-04-07 13:47:39 +01:00
"#,
)
.map_err(OneOf::new)?;
let config = ServerConfig::load(&fs)?;
let expected = ServerConfig {
webhook: Webhook {
url: "http://localhost:9909/webhook".to_string(),
},
storage: ServerStorage {
path: "/opt/git-next/data".into(),
},
forge: HashMap::from([(
"default".to_string(),
ForgeConfig {
forge_type: ForgeType::MockForge,
hostname: "git.example.net".to_string(),
user: "Bob".to_string(),
token: "API-Token".to_string(),
repos: HashMap::from([
(
"hello".to_string(),
ServerRepoConfig {
repo: "user/hello".to_string(),
branch: "main".to_string(),
gitdir: Some("/opt/git/user/hello.git".into()),
main: None,
next: None,
dev: None,
},
),
(
"world".to_string(),
ServerRepoConfig {
repo: "user/world".to_string(),
branch: "master".to_string(),
gitdir: None,
main: Some("main".to_string()),
next: Some("next".to_string()),
dev: Some("dev".to_string()),
},
),
(
"sam".to_string(),
ServerRepoConfig {
repo: "user/sam".to_string(),
branch: "main".to_string(),
gitdir: None,
main: Some("master".to_string()),
next: Some("upcoming".to_string()),
dev: Some("sam-dev".to_string()),
},
),
]),
},
)]),
};
assert_eq!(config, expected, "ServerConfig");
if let Some(forge) = config.forge.get("world") {
if let Some(repo) = forge.repos.get("sam") {
let repo_config = repo.repo_config();
let expected = Some(RepoConfig {
branches: RepoBranches {
main: "master".to_string(),
next: "upcoming".to_string(),
dev: "sam-dev".to_string(),
},
});
assert_eq!(repo_config, expected, "RepoConfig");
}
}
2024-04-07 13:47:39 +01:00
Ok(())
}
2024-04-08 09:54:20 +01:00
#[test]
fn test_repo_config_load() -> Result<(), OneOf<(toml::de::Error,)>> {
let toml = r#"
[branches]
main = "main"
next = "next"
dev = "dev"
[options]
"#;
let config = RepoConfig::load(toml)?;
assert_eq!(
config,
RepoConfig {
branches: RepoBranches {
main: "main".to_string(),
next: "next".to_string(),
dev: "dev".to_string(),
},
}
);
Ok(())
}
2024-04-07 13:47:39 +01:00
}