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

591 lines
16 KiB
Rust
Raw Normal View History

pub mod load;
use std::{
collections::HashMap,
fmt::{Display, Formatter},
ops::Deref,
path::{Path, PathBuf},
};
2024-04-07 13:47:39 +01:00
use serde::Deserialize;
2024-04-28 08:05:09 +01:00
use kxio::fs::FileSystem;
use tracing::info;
2024-04-28 08:05:09 +01:00
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
KxIoFs(kxio::fs::Error),
TomlDe(toml::de::Error),
}
impl std::error::Error for Error {}
type Result<T> = core::result::Result<T, Error>;
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> {
2024-04-28 08:05:09 +01:00
let str = fs.file_read_to_string(&fs.base().join("git-next-server.toml"))?;
toml::from_str(&str).map_err(Into::into)
}
pub(crate) fn forges(&self) -> impl Iterator<Item = (ForgeName, &ForgeConfig)> {
self.forge
.iter()
.map(|(name, forge)| (ForgeName(name.clone()), forge))
}
2024-04-23 07:09:30 +01:00
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<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,
}
impl ServerStorage {
pub fn path(&self) -> &Path {
self.path.as_path()
}
}
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::from_str(toml).map_err(Into::into)
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 {
pub fn repo(&self) -> RepoPath {
RepoPath(self.repo.clone())
}
#[allow(dead_code)]
pub fn branch(&self) -> BranchName {
BranchName(self.branch.clone())
}
pub fn gitdir(&self) -> Option<GitDir> {
self.gitdir.clone().map(GitDir::from)
}
/// 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)
}
}
impl From<&ForgeName> for PathBuf {
fn from(value: &ForgeName) -> Self {
Self::from(&value.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 secrecy::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 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(),
}
}
}
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 repo_alias: RepoAlias,
pub repo_path: RepoPath,
pub branch: BranchName,
pub forge: ForgeDetails,
pub repo_config: Option<RepoConfig>,
2024-04-23 07:09:30 +01:00
pub gitdir: GitDir,
}
impl RepoDetails {
pub fn new(
repo_alias: &RepoAlias,
server_repo_config: &ServerRepoConfig,
forge_name: &ForgeName,
2024-04-23 07:13:16 +01:00
forge_config: &ForgeConfig,
gitdir: GitDir,
) -> Self {
Self {
repo_alias: repo_alias.clone(),
repo_path: RepoPath(server_repo_config.repo.clone()),
repo_config: server_repo_config.repo_config(),
branch: BranchName(server_repo_config.branch.clone()),
gitdir,
forge: ForgeDetails {
forge_name: forge_name.clone(),
2024-04-23 07:13:16 +01:00
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<String> {
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()
}
#[allow(dead_code)]
pub fn validate_repo(&self) -> ValidationResult<()> {
self.gitdir.validate(self)
}
pub fn git_remote(&self) -> GitRemote {
GitRemote::new(self.forge.hostname.clone(), self.repo_path.clone())
}
}
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,
)
}
}
type ValidationResult<T> = core::result::Result<T, RepoValidationError>;
#[derive(Debug)]
pub enum RepoValidationError {
NoDefaultPushRemote,
NoUrlForDefaultPushRemote,
NoHostnameForDefaultPushRemote,
UnableToOpenRepo(String),
Io(std::io::Error),
MismatchDefaultPushRemote {
found: GitRemote,
expected: GitRemote,
},
MismatchDefaultFetchRemote {
found: GitRemote,
expected: GitRemote,
},
}
impl std::error::Error for RepoValidationError {}
impl Display for RepoValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoDefaultPushRemote => write!(f, "There is no default push remote"),
Self::NoUrlForDefaultPushRemote => write!(f, "The default push remote has no url"),
Self::NoHostnameForDefaultPushRemote => {
write!(f, "The default push remote has no hostname")
}
Self::UnableToOpenRepo(err) => write!(f, "Unable to open the git dir: {err}"),
Self::Io(err) => write!(f, "IO Error: {err:?}"),
Self::MismatchDefaultPushRemote { found, expected } => write!(
f,
"The default push remote doesn't match: {found}, expected: {expected}"
),
Self::MismatchDefaultFetchRemote { found, expected } => write!(
f,
"The default fetch remote doesn't match: {found}, expected: {expected}"
),
}
}
}
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)
}
}
2024-04-21 18:38:47 +01:00
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct GitDir(PathBuf);
impl GitDir {
2024-04-23 07:09:30 +01:00
#[cfg(test)]
pub(crate) fn new(pathbuf: &std::path::Path) -> Self {
info!("GitDir::new({pathbuf:?})");
2024-04-23 07:09:30 +01:00
Self(pathbuf.to_path_buf())
}
2024-04-21 18:38:47 +01:00
pub const fn pathbuf(&self) -> &PathBuf {
&self.0
}
pub fn validate(&self, repo_details: &RepoDetails) -> ValidationResult<()> {
let git_remote = repo_details.git_remote();
use gix::remote::Direction;
let push_remote = self.find_default_remote(Direction::Push)?;
let fetch_remote = self.find_default_remote(Direction::Fetch)?;
info!(gitdir = %self, ?git_remote, ?push_remote, ?fetch_remote, "Gitdir::validate");
if git_remote != push_remote {
return Err(RepoValidationError::MismatchDefaultPushRemote {
found: push_remote,
expected: git_remote,
});
}
if git_remote != fetch_remote {
return Err(RepoValidationError::MismatchDefaultFetchRemote {
found: fetch_remote,
expected: git_remote,
});
}
Ok(())
}
#[tracing::instrument]
fn find_default_remote(
&self,
direction: gix::remote::Direction,
) -> ValidationResult<GitRemote> {
let repository = gix::ThreadSafeRepository::open(self.deref())
.map_err(|e| RepoValidationError::UnableToOpenRepo(e.to_string()))?
.to_thread_local();
info!(?repository, from = ?self.deref(), "gix::discover");
let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else {
return Err(RepoValidationError::NoDefaultPushRemote);
};
let Some(url) = remote.url(direction) else {
return Err(RepoValidationError::NoUrlForDefaultPushRemote);
};
let Some(host) = url.host() else {
return Err(RepoValidationError::NoHostnameForDefaultPushRemote);
};
let path = url.path.to_string();
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
let path = path.strip_suffix(".git").map_or(path, |path| path);
info!(%host, %path, "found");
Ok(GitRemote::new(
Hostname(host.to_string()),
RepoPath(path.to_string()),
))
}
2024-04-21 18:38:47 +01:00
}
2024-04-24 06:42:21 +01:00
impl Deref for GitDir {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
2024-04-21 18:38:47 +01:00
impl std::fmt::Display for GitDir {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0.display())
2024-04-21 18:38:47 +01:00
}
}
impl From<&str> for GitDir {
fn from(value: &str) -> Self {
info!("GitDir::from::<&str>({value:?})");
2024-04-21 18:38:47 +01:00
Self(value.into())
}
}
impl From<PathBuf> for GitDir {
fn from(value: PathBuf) -> Self {
info!("GitDir::from::<PathBuf>({value:?})");
Self(value)
}
}
impl From<GitDir> for PathBuf {
fn from(value: GitDir) -> Self {
value.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GitRemote {
host: Hostname,
repo_path: RepoPath,
}
impl GitRemote {
pub const fn new(host: Hostname, repo_path: RepoPath) -> Self {
Self { host, repo_path }
}
pub const fn host(&self) -> &Hostname {
&self.host
}
pub const fn repo_path(&self) -> &RepoPath {
&self.repo_path
}
}
impl Display for GitRemote {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.host, self.repo_path)
}
}
2024-04-21 18:38:47 +01:00
2024-04-07 13:47:39 +01:00
#[cfg(test)]
mod tests;