diff --git a/Cargo.toml b/Cargo.toml index d7d9147..dce17b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/cli", "crates/server"] +members = ["crates/cli", "crates/server", "crates/config", "crates/git"] [workspace.package] version = "0.3.0" @@ -14,6 +14,8 @@ expect_used = "warn" [workspace.dependencies] git-next-server = { path = "crates/server" } +git-next-config = { path = "crates/config" } +git-next-git = { path = "crates/git" } # CLI parsing clap = { version = "4.5", features = ["cargo", "derive"] } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml new file mode 100644 index 0000000..f0fd3cb --- /dev/null +++ b/crates/config/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "git-next-config" +version = { workspace = true } +edition = { workspace = true } + +[features] +default = ["forgejo"] +forgejo = [] +github = [] + +[dependencies] +# # logging +# console-subscriber = { workspace = true } +# tracing = { workspace = true } +# tracing-subscriber = { workspace = true } + +# # base64 decoding +# base64 = { workspace = true } +# +# # git +# # gix = { workspace = true } +# gix = { workspace = true } +# async-trait = { workspace = true } +# +# # fs/network +# kxio = { workspace = true } +# +# # fs +# tempfile = { workspace = true } + +# TOML parsing +serde = { workspace = true } +# serde_json = { workspace = true } +toml = { workspace = true } + +# Secrets and Password +secrecy = { workspace = true } + +# # Conventional Commit check +# git-conventional = { workspace = true } +# +# # Webhooks +# bytes = { workspace = true } +# ulid = { workspace = true } +# warp = { workspace = true } +# +# # error handling +# derive_more = { workspace = true } +# terrors = { workspace = true } +# +# # file watcher +# inotify = { workspace = true } +# +# # Actors +# actix = { workspace = true } +# actix-rt = { workspace = true } +# tokio = { workspace = true } +# +# [dev-dependencies] +# # Testing +# assert2 = { workspace = true } +# pretty_assertions = { workspace = true } +# test-log = { workspace = true } +# anyhow = { workspace = true } + +[lints.clippy] +nursery = { level = "warn", priority = -1 } +# pedantic = "warn" +unwrap_used = "warn" +expect_used = "warn" diff --git a/crates/config/src/api_token.rs b/crates/config/src/api_token.rs new file mode 100644 index 0000000..1d2acfb --- /dev/null +++ b/crates/config/src/api_token.rs @@ -0,0 +1,16 @@ +/// 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() + } +} diff --git a/crates/config/src/branch_name.rs b/crates/config/src/branch_name.rs new file mode 100644 index 0000000..49e6017 --- /dev/null +++ b/crates/config/src/branch_name.rs @@ -0,0 +1,17 @@ +use std::fmt::Formatter; + +/// The name of a Branch +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct BranchName(pub String); +impl std::fmt::Display for BranchName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl std::ops::Deref for BranchName { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/config/src/forge_config.rs b/crates/config/src/forge_config.rs new file mode 100644 index 0000000..b352dc5 --- /dev/null +++ b/crates/config/src/forge_config.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User}; + +/// Defines a Forge to connect to +/// Maps from `git-next-server.toml` at `forge.{forge}` +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct ForgeConfig { + pub forge_type: ForgeType, + pub hostname: String, + pub user: String, + pub token: String, + pub 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 std::fmt::Display for ForgeConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}@{}", + self.forge_type.to_string().to_lowercase(), + self.user, + self.hostname + ) + } +} diff --git a/crates/config/src/forge_details.rs b/crates/config/src/forge_details.rs new file mode 100644 index 0000000..9636893 --- /dev/null +++ b/crates/config/src/forge_details.rs @@ -0,0 +1,24 @@ +use crate::{ApiToken, ForgeConfig, ForgeName, ForgeType, Hostname, User}; + +/// 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(), + } + } +} diff --git a/crates/config/src/forge_name.rs b/crates/config/src/forge_name.rs new file mode 100644 index 0000000..b55bd77 --- /dev/null +++ b/crates/config/src/forge_name.rs @@ -0,0 +1,15 @@ +use std::path::PathBuf; + +/// The name of a Forge to connect to +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ForgeName(pub String); +impl std::fmt::Display for ForgeName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl From<&ForgeName> for PathBuf { + fn from(value: &ForgeName) -> Self { + Self::from(&value.0) + } +} diff --git a/crates/config/src/forge_type.rs b/crates/config/src/forge_type.rs new file mode 100644 index 0000000..74d119d --- /dev/null +++ b/crates/config/src/forge_type.rs @@ -0,0 +1,16 @@ +/// Identifier for the type of Forge +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] +pub enum ForgeType { + #[cfg(feature = "forgejo")] + ForgeJo, + // Gitea, + // GitHub, + // GitLab, + // BitBucket, + MockForge, +} +impl std::fmt::Display for ForgeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format!("{:?}", self).to_lowercase()) + } +} diff --git a/crates/config/src/git_dir.rs b/crates/config/src/git_dir.rs new file mode 100644 index 0000000..ebbd4ee --- /dev/null +++ b/crates/config/src/git_dir.rs @@ -0,0 +1,45 @@ +use std::{ops::Deref, path::PathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +pub struct GitDir(PathBuf); +impl GitDir { + pub fn new(pathbuf: &std::path::Path) -> Self { + Self(pathbuf.to_path_buf()) + } + + pub const fn pathbuf(&self) -> &PathBuf { + &self.0 + } +} +impl Deref for GitDir { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::fmt::Display for GitDir { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0.display()) + } +} +impl From<&str> for GitDir { + fn from(value: &str) -> Self { + Self(value.into()) + } +} +impl From<&GitDir> for PathBuf { + fn from(value: &GitDir) -> Self { + value.to_path_buf() + } +} +impl From for GitDir { + fn from(value: PathBuf) -> Self { + Self(value) + } +} +impl From for PathBuf { + fn from(value: GitDir) -> Self { + value.0 + } +} diff --git a/crates/config/src/host_name.rs b/crates/config/src/host_name.rs new file mode 100644 index 0000000..b8943a1 --- /dev/null +++ b/crates/config/src/host_name.rs @@ -0,0 +1,10 @@ +use std::fmt::{Display, Formatter}; + +/// 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) + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs new file mode 100644 index 0000000..a284394 --- /dev/null +++ b/crates/config/src/lib.rs @@ -0,0 +1,32 @@ +// +mod api_token; +mod branch_name; +mod forge_config; +mod forge_details; +mod forge_name; +mod forge_type; +pub mod git_dir; +mod host_name; +mod repo_alias; +mod repo_branches; +mod repo_config; +mod repo_config_source; +mod repo_path; +mod server_repo_config; +mod user; + +pub use api_token::ApiToken; +pub use branch_name::BranchName; +pub use forge_config::ForgeConfig; +pub use forge_details::ForgeDetails; +pub use forge_name::ForgeName; +pub use forge_type::ForgeType; +pub use git_dir::GitDir; +pub use host_name::Hostname; +pub use repo_alias::RepoAlias; +pub use repo_branches::RepoBranches; +pub use repo_config::RepoConfig; +pub use repo_config_source::RepoConfigSource; +pub use repo_path::RepoPath; +pub use server_repo_config::ServerRepoConfig; +pub use user::User; diff --git a/crates/config/src/repo_alias.rs b/crates/config/src/repo_alias.rs new file mode 100644 index 0000000..08ebb27 --- /dev/null +++ b/crates/config/src/repo_alias.rs @@ -0,0 +1,9 @@ +/// 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 std::fmt::Display for RepoAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/config/src/repo_branches.rs b/crates/config/src/repo_branches.rs new file mode 100644 index 0000000..6f87c57 --- /dev/null +++ b/crates/config/src/repo_branches.rs @@ -0,0 +1,34 @@ +use crate::BranchName; + +/// Mapped from `.git-next.toml` file at `branches` +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] +pub struct RepoBranches { + pub main: String, + pub next: String, + pub dev: String, +} +impl RepoBranches { + 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 std::fmt::Display for RepoBranches { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{},{},{}", self.main, self.next, self.dev) + } +} diff --git a/crates/config/src/repo_config.rs b/crates/config/src/repo_config.rs new file mode 100644 index 0000000..4d0e795 --- /dev/null +++ b/crates/config/src/repo_config.rs @@ -0,0 +1,33 @@ +use crate::RepoBranches; +use crate::RepoConfigSource; + +/// 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, serde::Deserialize)] +pub struct RepoConfig { + pub branches: RepoBranches, + pub source: RepoConfigSource, +} +impl RepoConfig { + pub const fn new(branches: RepoBranches, source: RepoConfigSource) -> Self { + Self { branches, source } + } + + pub fn load(toml: &str) -> Result { + toml::from_str(format!("source = \"Repo\"\n{}", toml).as_str()) + } + + pub const fn branches(&self) -> &RepoBranches { + &self.branches + } + + pub const fn source(&self) -> RepoConfigSource { + self.source + } +} +impl std::fmt::Display for RepoConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.branches) + } +} diff --git a/crates/config/src/repo_config_source.rs b/crates/config/src/repo_config_source.rs new file mode 100644 index 0000000..67263d8 --- /dev/null +++ b/crates/config/src/repo_config_source.rs @@ -0,0 +1,5 @@ +#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize)] +pub enum RepoConfigSource { + Repo, + Server, +} diff --git a/crates/config/src/repo_path.rs b/crates/config/src/repo_path.rs new file mode 100644 index 0000000..8c7ff16 --- /dev/null +++ b/crates/config/src/repo_path.rs @@ -0,0 +1,12 @@ +use std::fmt::{Display, Formatter}; + +/// 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) + } +} diff --git a/crates/config/src/server_repo_config.rs b/crates/config/src/server_repo_config.rs new file mode 100644 index 0000000..38ca757 --- /dev/null +++ b/crates/config/src/server_repo_config.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +use crate::{BranchName, GitDir, RepoBranches, RepoConfig, RepoConfigSource, RepoPath}; + +/// 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, serde::Deserialize)] +pub struct ServerRepoConfig { + pub repo: String, + pub branch: String, + pub gitdir: Option, + pub main: Option, + pub next: Option, + pub dev: Option, +} +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 { + 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 { + 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(), + }, + source: RepoConfigSource::Server, + }), + _ => None, + } + } +} +#[cfg(test)] +impl AsRef for ServerRepoConfig { + fn as_ref(&self) -> &Self { + self + } +} +impl std::fmt::Display for ServerRepoConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.repo, self.branch) + } +} diff --git a/crates/config/src/user.rs b/crates/config/src/user.rs new file mode 100644 index 0000000..e65bc71 --- /dev/null +++ b/crates/config/src/user.rs @@ -0,0 +1,8 @@ +/// The user within the forge to connect as +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct User(pub String); +impl std::fmt::Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml new file mode 100644 index 0000000..201a0ee --- /dev/null +++ b/crates/git/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "git-next-git" +version = { workspace = true } +edition = { workspace = true } + +[features] +default = ["forgejo"] +forgejo = [] +github = [] + +[dependencies] +git-next-config = { workspace = true } + +# logging +# console-subscriber = { workspace = true } +tracing = { workspace = true } +# tracing-subscriber = { workspace = true } + +# # base64 decoding +# base64 = { workspace = true } +# +# git +# # gix = { workspace = true } +gix = { workspace = true } +# async-trait = { workspace = true } +# +# # fs/network +# kxio = { workspace = true } +# +# # fs +# tempfile = { workspace = true } + +# # TOML parsing +# serde = { workspace = true } +# # serde_json = { workspace = true } +# toml = { workspace = true } + +# Secrets and Password +secrecy = { workspace = true } + +# # Conventional Commit check +# git-conventional = { workspace = true } +# +# # Webhooks +# bytes = { workspace = true } +# ulid = { workspace = true } +# warp = { workspace = true } + +# error handling +derive_more = { workspace = true } +# terrors = { workspace = true } + +# # file watcher +# inotify = { workspace = true } +# +# # Actors +# actix = { workspace = true } +# actix-rt = { workspace = true } +# tokio = { workspace = true } +# +# [dev-dependencies] +# # Testing +# assert2 = { workspace = true } +# pretty_assertions = { workspace = true } +# test-log = { workspace = true } +# anyhow = { workspace = true } + +[lints.clippy] +nursery = { level = "warn", priority = -1 } +# pedantic = "warn" +unwrap_used = "warn" +expect_used = "warn" diff --git a/crates/git/src/commit.rs b/crates/git/src/commit.rs new file mode 100644 index 0000000..390046a --- /dev/null +++ b/crates/git/src/commit.rs @@ -0,0 +1,50 @@ +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Commit { + sha: Sha, + message: Message, +} +impl Commit { + pub fn new(sha: &str, message: &str) -> Self { + Self { + sha: Sha::new(sha.to_string()), + message: Message::new(message.to_string()), + } + } + pub const fn sha(&self) -> &Sha { + &self.sha + } + pub const fn message(&self) -> &Message { + &self.message + } +} +impl std::fmt::Display for Commit { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.sha) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Sha(String); +impl Sha { + pub const fn new(value: String) -> Self { + Self(value) + } +} +impl std::fmt::Display for Sha { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Message(String); +impl Message { + pub const fn new(value: String) -> Self { + Self(value) + } +} +impl std::fmt::Display for Message { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/server/src/gitforge/forgejo/branch/fetch.rs b/crates/git/src/fetch.rs similarity index 95% rename from crates/server/src/gitforge/forgejo/branch/fetch.rs rename to crates/git/src/fetch.rs index a901058..4d561de 100644 --- a/crates/server/src/gitforge/forgejo/branch/fetch.rs +++ b/crates/git/src/fetch.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use tracing::{debug, info}; -use crate::{config::RepoDetails, gitforge::Repository}; +use super::{RepoDetails, Repository}; #[derive(Debug, derive_more::From, derive_more::Display)] pub enum Error { diff --git a/crates/git/src/generation.rs b/crates/git/src/generation.rs new file mode 100644 index 0000000..5e0e0e4 --- /dev/null +++ b/crates/git/src/generation.rs @@ -0,0 +1,15 @@ +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Generation(u32); +impl Generation { + pub fn new() -> Self { + Self::default() + } + pub fn inc(&mut self) { + self.0 += 1 + } +} +impl std::fmt::Display for Generation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/git/src/git_ref.rs b/crates/git/src/git_ref.rs new file mode 100644 index 0000000..520860e --- /dev/null +++ b/crates/git/src/git_ref.rs @@ -0,0 +1,21 @@ +use git_next_config::BranchName; + +use crate::Commit; + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct GitRef(pub String); +impl From for GitRef { + fn from(value: Commit) -> Self { + Self(value.sha().to_string()) + } +} +impl From for GitRef { + fn from(value: BranchName) -> Self { + Self(value.0) + } +} +impl std::fmt::Display for GitRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/git/src/git_remote.rs b/crates/git/src/git_remote.rs new file mode 100644 index 0000000..e234004 --- /dev/null +++ b/crates/git/src/git_remote.rs @@ -0,0 +1,23 @@ +use git_next_config::{Hostname, RepoPath}; + +#[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 std::fmt::Display for GitRemote { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.host, self.repo_path) + } +} diff --git a/crates/git/src/lib.rs b/crates/git/src/lib.rs new file mode 100644 index 0000000..b4627df --- /dev/null +++ b/crates/git/src/lib.rs @@ -0,0 +1,20 @@ +// +pub mod commit; +pub mod fetch; +mod generation; +mod git_ref; +mod git_remote; +mod repo_details; +pub mod repository; +pub mod reset; +pub mod validate; + +pub use commit::Commit; +pub use fetch::fetch; +pub use generation::Generation; +pub use git_ref::GitRef; +pub use git_remote::GitRemote; +pub use repo_details::RepoDetails; +pub use repository::Repository; +pub use reset::reset; +pub use validate::validate; diff --git a/crates/git/src/repo_details.rs b/crates/git/src/repo_details.rs new file mode 100644 index 0000000..bb0aaff --- /dev/null +++ b/crates/git/src/repo_details.rs @@ -0,0 +1,75 @@ +use git_next_config::{ + BranchName, ForgeConfig, ForgeDetails, ForgeName, GitDir, RepoAlias, RepoConfig, RepoPath, + ServerRepoConfig, +}; + +use super::{Generation, GitRemote}; + +/// The derived information about a repo, used to interact with it +#[derive(Clone, Debug)] +pub struct RepoDetails { + pub generation: Generation, + 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( + generation: Generation, + repo_alias: &RepoAlias, + server_repo_config: &ServerRepoConfig, + forge_name: &ForgeName, + forge_config: &ForgeConfig, + gitdir: GitDir, + ) -> Self { + Self { + generation, + 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(), + 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() + } + + pub fn git_remote(&self) -> GitRemote { + GitRemote::new(self.forge.hostname.clone(), self.repo_path.clone()) + } +} +impl std::fmt::Display for RepoDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "gen-{}:{}:{}/{}:{}@{}/{}@{}", + self.generation, + self.forge.forge_type, + self.forge.forge_name, + self.repo_alias, + self.forge.user, + self.forge.hostname, + self.repo_path, + self.branch, + ) + } +} diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs new file mode 100644 index 0000000..3dbccca --- /dev/null +++ b/crates/git/src/repository.rs @@ -0,0 +1,70 @@ +// +use std::{ops::Deref as _, path::PathBuf, sync::atomic::AtomicBool}; + +use super::RepoDetails; + +#[derive(Debug, Clone, derive_more::From)] +pub struct Repository(gix::ThreadSafeRepository); +impl Repository { + pub fn open(gitdir: impl Into) -> Result { + Ok(Self(gix::ThreadSafeRepository::open(gitdir.into())?)) + } + pub fn clone(repo_details: &RepoDetails) -> Result { + use secrecy::ExposeSecret; + let origin = repo_details.origin(); + let (repository, _outcome) = + gix::prepare_clone_bare(origin.expose_secret().as_str(), repo_details.gitdir.deref())? + .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; + + Ok(repository.into_sync().into()) + } +} +impl std::ops::Deref for Repository { + type Target = gix::ThreadSafeRepository; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub enum Error { + InvalidGitDir(git_next_config::GitDir), + Io(std::io::Error), + Wait(std::io::Error), + Spawn(std::io::Error), + Validation(String), + GixClone(Box), + GixOpen(Box), + GixFetch(Box), +} +impl std::error::Error for Error {} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidGitDir(gitdir) => write!(f, "Invalid Git dir: {:?}", gitdir), + Self::Io(err) => write!(f, "IO error: {:?}", err), + Self::Wait(err) => write!(f, "Waiting for command: {:?}", err), + Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err), + Self::Validation(err) => write!(f, "Validation: {}", err), + Self::GixClone(err) => write!(f, "Clone: {:?}", err), + Self::GixOpen(err) => write!(f, "Open: {:?}", err), + Self::GixFetch(err) => write!(f, "Fetch: {:?}", err), + } + } +} +impl From for Error { + fn from(value: gix::clone::Error) -> Self { + Self::GixClone(Box::new(value)) + } +} +impl From for Error { + fn from(value: gix::open::Error) -> Self { + Self::GixOpen(Box::new(value)) + } +} +impl From for Error { + fn from(value: gix::clone::fetch::Error) -> Self { + Self::GixFetch(Box::new(value)) + } +} diff --git a/crates/server/src/git/reset.rs b/crates/git/src/reset.rs similarity index 69% rename from crates/server/src/git/reset.rs rename to crates/git/src/reset.rs index 01dd4cd..2fe4cbd 100644 --- a/crates/server/src/git/reset.rs +++ b/crates/git/src/reset.rs @@ -1,13 +1,24 @@ use std::ops::Deref; +use git_next_config::BranchName; use secrecy::ExposeSecret; use tracing::{info, warn}; -use crate::{ - config::{BranchName, RepoDetails}, - gitforge::{BranchResetError, BranchResetResult, Force, Repository}, - types::GitRef, -}; +use super::{GitRef, RepoDetails, Repository}; + +#[derive(Debug)] +pub enum Force { + No, + From(GitRef), +} +impl std::fmt::Display for Force { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::No => write!(f, "fast-foward"), + Self::From(from) => write!(f, "force-if-from:{}", from), + } + } +} // TODO: (#72) reimplement using `gix` #[tracing::instrument(skip_all, fields(branch = %branch_name, to = %to_commit, force = %force))] @@ -17,7 +28,7 @@ pub fn reset( branch_name: BranchName, to_commit: GitRef, force: Force, -) -> BranchResetResult { +) -> Result { let origin = repo_details.origin(); let force = match force { Force::No => "".to_string(), @@ -56,3 +67,13 @@ pub fn reset( } } } + +#[derive(Debug, derive_more::From, derive_more::Display)] +pub enum BranchResetError { + Open(Box), + Fetch(super::fetch::Error), + Push, +} +impl std::error::Error for BranchResetError {} + +pub type Result = core::result::Result<(), BranchResetError>; diff --git a/crates/git/src/validate.rs b/crates/git/src/validate.rs new file mode 100644 index 0000000..fa352b5 --- /dev/null +++ b/crates/git/src/validate.rs @@ -0,0 +1,93 @@ +use std::ops::Deref as _; + +use git_next_config::{Hostname, RepoPath}; +use gix::remote::Direction; +use tracing::info; + +use super::{GitRemote, RepoDetails, Repository}; + +#[tracing::instrument(skip_all)] +pub fn validate(repository: &Repository, repo_details: &RepoDetails) -> Result<()> { + let git_remote = repo_details.git_remote(); + let push_remote = find_default_remote(repository, Direction::Push)?; + let fetch_remote = find_default_remote(repository, Direction::Fetch)?; + info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match"); + if git_remote != push_remote { + return Err(Error::MismatchDefaultPushRemote { + found: push_remote, + expected: git_remote, + }); + } + if git_remote != fetch_remote { + return Err(Error::MismatchDefaultFetchRemote { + found: fetch_remote, + expected: git_remote, + }); + } + Ok(()) +} + +#[tracing::instrument(skip_all, fields(?direction))] +pub fn find_default_remote( + repository: &Repository, + direction: gix::remote::Direction, +) -> Result { + let repository = repository.deref().to_thread_local(); + let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else { + return Err(Error::NoDefaultPushRemote); + }; + let Some(url) = remote.url(direction) else { + return Err(Error::NoUrlForDefaultPushRemote); + }; + let Some(host) = url.host() else { + return Err(Error::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()), + )) +} + +type Result = core::result::Result; +#[derive(Debug)] +pub enum Error { + NoDefaultPushRemote, + NoUrlForDefaultPushRemote, + NoHostnameForDefaultPushRemote, + UnableToOpenRepo(String), + Io(std::io::Error), + MismatchDefaultPushRemote { + found: GitRemote, + expected: GitRemote, + }, + MismatchDefaultFetchRemote { + found: GitRemote, + expected: GitRemote, + }, +} +impl std::error::Error for Error {} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::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}" + ), + } + } +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 951301f..3c5e5d8 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -9,6 +9,9 @@ forgejo = [] github = [] [dependencies] +git-next-config = { workspace = true } +git-next-git = { workspace = true } + # logging console-subscriber = { workspace = true } tracing = { workspace = true } diff --git a/crates/server/src/actors/repo/branch.rs b/crates/server/src/actors/repo/branch.rs index 1f2e3f5..24baab4 100644 --- a/crates/server/src/actors/repo/branch.rs +++ b/crates/server/src/actors/repo/branch.rs @@ -2,22 +2,23 @@ use std::time::Duration; use actix::prelude::*; +use git_next_config::{RepoConfig, RepoConfigSource}; +use git_next_git as git; use tracing::{info, warn}; use crate::{ actors::repo::{LoadConfigFromRepo, RepoActor, ValidateRepo}, - config::{self, RepoConfigSource}, gitforge, }; // advance next to the next commit towards the head of the dev branch #[tracing::instrument(fields(next), skip_all)] pub async fn advance_next( - next: gitforge::Commit, - dev_commit_history: Vec, - repo_config: config::RepoConfig, + next: git::Commit, + dev_commit_history: Vec, + repo_config: RepoConfig, forge: gitforge::Forge, - repository: gitforge::Repository, + repository: git::Repository, addr: Addr, message_token: super::MessageToken, ) { @@ -35,7 +36,7 @@ pub async fn advance_next( &repository, repo_config.branches().next(), commit.into(), - gitforge::Force::No, + git::reset::Force::No, ) { warn!(?err, "Failed") } @@ -44,7 +45,7 @@ pub async fn advance_next( } #[tracing::instrument] -fn validate_commit_message(message: &gitforge::Message) -> Option { +fn validate_commit_message(message: &git::commit::Message) -> Option { let message = &message.to_string(); if message.to_ascii_lowercase().starts_with("wip") { return Some("Is Work-In-Progress".to_string()); @@ -62,10 +63,10 @@ fn validate_commit_message(message: &gitforge::Message) -> Option { } fn find_next_commit_on_dev( - next: gitforge::Commit, - dev_commit_history: Vec, -) -> Option { - let mut next_commit: Option = None; + next: git::Commit, + dev_commit_history: Vec, +) -> Option { + let mut next_commit: Option = None; for commit in dev_commit_history.into_iter() { if commit == next { break; @@ -78,10 +79,10 @@ fn find_next_commit_on_dev( // advance main branch to the commit 'next' #[tracing::instrument(fields(next), skip_all)] pub async fn advance_main( - next: gitforge::Commit, - repo_config: config::RepoConfig, + next: git::Commit, + repo_config: RepoConfig, forge: gitforge::Forge, - repository: gitforge::Repository, + repository: git::Repository, addr: Addr, message_token: super::MessageToken, ) { @@ -90,7 +91,7 @@ pub async fn advance_main( &repository, repo_config.branches().main(), next.into(), - gitforge::Force::No, + git::reset::Force::No, ) { warn!(?err, "Failed") }; @@ -106,13 +107,13 @@ mod tests { #[actix_rt::test] async fn test_find_next_commit_on_dev() { - let next = gitforge::Commit::new("current-next", "foo"); - let expected = gitforge::Commit::new("dev-next", "next-should-go-here"); + let next = git::Commit::new("current-next", "foo"); + let expected = git::Commit::new("dev-next", "next-should-go-here"); let dev_commit_history = vec![ - gitforge::Commit::new("dev", "future"), + git::Commit::new("dev", "future"), expected.clone(), next.clone(), - gitforge::Commit::new("current-main", "history"), + git::Commit::new("current-main", "history"), ]; let next_commit = find_next_commit_on_dev(next, dev_commit_history); assert_eq!(next_commit, Some(expected)); diff --git a/crates/server/src/actors/repo/config.rs b/crates/server/src/actors/repo/config.rs index 12cf70e..10dd68d 100644 --- a/crates/server/src/actors/repo/config.rs +++ b/crates/server/src/actors/repo/config.rs @@ -1,4 +1,5 @@ use actix::prelude::*; +use git_next_git::RepoDetails; use tracing::{error, info}; use crate::{config, gitforge}; @@ -7,11 +8,7 @@ use super::{LoadedConfig, RepoActor}; /// Loads the [RepoConfig] from the `.git-next.toml` file in the repository #[tracing::instrument(skip_all, fields(branch = %repo_details.branch))] -pub async fn load( - repo_details: config::RepoDetails, - addr: Addr, - forge: gitforge::Forge, -) { +pub async fn load(repo_details: RepoDetails, addr: Addr, forge: gitforge::Forge) { info!("Loading .git-next.toml from repo"); let repo_config = match config::load::load(&repo_details, &forge).await { Ok(repo_config) => repo_config, diff --git a/crates/server/src/actors/repo/mod.rs b/crates/server/src/actors/repo/mod.rs index 824756c..31afbee 100644 --- a/crates/server/src/actors/repo/mod.rs +++ b/crates/server/src/actors/repo/mod.rs @@ -4,46 +4,40 @@ pub mod status; pub mod webhook; use actix::prelude::*; +use git_next_config::{ForgeType, RepoConfig}; +use git_next_git::{self as git, Generation, RepoDetails}; use kxio::network::Network; use tracing::{debug, info, warn, Instrument}; -use crate::{ - actors::repo::webhook::WebhookAuth, - config::{RepoConfig, RepoDetails, Webhook}, - gitforge::{self, Repository}, - types::{MessageToken, ServerGeneration}, -}; +use crate::{actors::repo::webhook::WebhookAuth, config::Webhook, gitforge, types::MessageToken}; use self::webhook::WebhookId; pub struct RepoActor { - generation: ServerGeneration, + generation: Generation, message_token: MessageToken, details: RepoDetails, webhook: Webhook, webhook_id: Option, // INFO: if [None] then no webhook is configured webhook_auth: Option, // INFO: if [None] then no webhook is configured - last_main_commit: Option, - last_next_commit: Option, - last_dev_commit: Option, - repository: Option, + last_main_commit: Option, + last_next_commit: Option, + last_dev_commit: Option, + repository: Option, net: Network, forge: gitforge::Forge, } impl RepoActor { - pub(crate) fn new( + pub fn new( details: RepoDetails, webhook: Webhook, - generation: ServerGeneration, + generation: Generation, net: Network, ) -> Self { let forge = match details.forge.forge_type { #[cfg(feature = "forgejo")] - crate::config::ForgeType::ForgeJo => { - gitforge::Forge::new_forgejo(details.clone(), net.clone()) - } - #[cfg(test)] - crate::config::ForgeType::MockForge => gitforge::Forge::new_mock(), + ForgeType::ForgeJo => gitforge::Forge::new_forgejo(details.clone(), net.clone()), + ForgeType::MockForge => gitforge::Forge::new_mock(), }; debug!(?forge, "new"); Self { @@ -212,10 +206,10 @@ impl Handler for RepoActor { #[derive(Debug, Message)] #[rtype(result = "()")] pub struct StartMonitoring { - pub main: gitforge::Commit, - pub next: gitforge::Commit, - pub dev: gitforge::Commit, - pub dev_commit_history: Vec, + pub main: git::Commit, + pub next: git::Commit, + pub dev: git::Commit, + pub dev_commit_history: Vec, } impl Handler for RepoActor { type Result = (); @@ -273,7 +267,7 @@ impl Handler for RepoActor { #[derive(Message)] #[rtype(result = "()")] -pub struct AdvanceMainTo(pub gitforge::Commit); +pub struct AdvanceMainTo(pub git::Commit); impl Handler for RepoActor { type Result = (); #[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.details, commit = %msg.0))] diff --git a/crates/server/src/actors/repo/status.rs b/crates/server/src/actors/repo/status.rs index 9064bb2..92af757 100644 --- a/crates/server/src/actors/repo/status.rs +++ b/crates/server/src/actors/repo/status.rs @@ -1,4 +1,5 @@ use actix::prelude::*; +use git_next_git as git; use gix::trace::warn; use tracing::info; @@ -7,7 +8,7 @@ use crate::{actors::repo::ValidateRepo, gitforge, types::MessageToken}; use super::AdvanceMainTo; pub async fn check_next( - next: gitforge::Commit, + next: git::Commit, addr: Addr, forge: gitforge::Forge, message_token: MessageToken, diff --git a/crates/server/src/actors/repo/webhook.rs b/crates/server/src/actors/repo/webhook.rs index f23e2b2..bdf20fa 100644 --- a/crates/server/src/actors/repo/webhook.rs +++ b/crates/server/src/actors/repo/webhook.rs @@ -1,4 +1,6 @@ use actix::prelude::*; +use git_next_config::RepoBranches; +use git_next_git::{self as git, RepoDetails}; use kxio::network::{self, json}; use tracing::{debug, info, warn}; use ulid::DecodeError; @@ -10,8 +12,7 @@ use crate::{ repo::{RepoActor, ValidateRepo, WebhookRegistered}, webhook::WebhookMessage, }, - config::{RepoBranches, Webhook, WebhookUrl}, - gitforge, + config::{Webhook, WebhookUrl}, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -58,11 +59,7 @@ impl Deref for WebhookAuth { } #[tracing::instrument(skip_all, fields(%webhook_id))] -pub async fn unregister( - webhook_id: WebhookId, - repo_details: crate::config::RepoDetails, - net: network::Network, -) { +pub async fn unregister(webhook_id: WebhookId, repo_details: RepoDetails, net: network::Network) { let hostname = &repo_details.forge.hostname; let repo_path = repo_details.repo_path; use secrecy::ExposeSecret; @@ -88,7 +85,7 @@ pub async fn unregister( #[tracing::instrument(skip_all)] pub async fn register( - repo_details: crate::config::RepoDetails, + repo_details: RepoDetails, webhook: Webhook, addr: actix::prelude::Addr, net: network::Network, @@ -147,7 +144,7 @@ pub async fn register( } async fn find_existing_webhooks( - repo_details: &crate::config::RepoDetails, + repo_details: &RepoDetails, webhook_url: &WebhookUrl, net: &network::Network, ) -> Vec { @@ -301,8 +298,8 @@ impl Push { warn!(branch, "Unexpected branch"); None } - pub fn commit(&self) -> gitforge::Commit { - gitforge::Commit::new(&self.after, &self.head_commit.message) + pub fn commit(&self) -> git::Commit { + git::Commit::new(&self.after, &self.head_commit.message) } } diff --git a/crates/server/src/actors/server.rs b/crates/server/src/actors/server.rs index 74789d3..0de8edb 100644 --- a/crates/server/src/actors/server.rs +++ b/crates/server/src/actors/server.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use actix::prelude::*; +use git_next_config::{ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig}; +use git_next_git::{Generation, RepoDetails}; use kxio::{fs::FileSystem, network::Network}; use tracing::{error, info, warn}; @@ -11,11 +13,7 @@ use crate::{ repo::{CloneRepo, RepoActor}, webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter}, }, - config::{ - ForgeConfig, ForgeName, GitDir, RepoAlias, RepoDetails, ServerConfig, ServerRepoConfig, - ServerStorage, Webhook, - }, - types::ServerGeneration, + config::{ServerConfig, ServerStorage, Webhook}, }; #[derive(Debug, derive_more::Display, derive_more::From)] @@ -35,7 +33,7 @@ pub enum Error { type Result = core::result::Result; pub struct Server { - generation: ServerGeneration, + generation: Generation, webhook: Option>, fs: FileSystem, net: Network, @@ -113,7 +111,7 @@ impl Handler for Server { } impl Server { pub fn new(fs: FileSystem, net: Network) -> Self { - let generation = ServerGeneration::new(); + let generation = Generation::new(); Self { generation, webhook: None, diff --git a/crates/server/src/actors/webhook/router.rs b/crates/server/src/actors/webhook/router.rs index 1e4ede5..84e662d 100644 --- a/crates/server/src/actors/webhook/router.rs +++ b/crates/server/src/actors/webhook/router.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use actix::prelude::*; +use git_next_config::RepoAlias; use tracing::{debug, info}; -use crate::{actors::webhook::message::WebhookMessage, config::RepoAlias}; +use crate::actors::webhook::message::WebhookMessage; pub struct WebhookRouter { span: tracing::Span, diff --git a/crates/server/src/config/load.rs b/crates/server/src/config/load.rs index 7a59799..1d63c16 100644 --- a/crates/server/src/config/load.rs +++ b/crates/server/src/config/load.rs @@ -1,10 +1,9 @@ +use git_next_config::{BranchName, RepoConfig}; +use git_next_git::RepoDetails; use terrors::OneOf; use tracing::error; -use crate::{ - config::{BranchName, RepoConfig, RepoDetails}, - gitforge::{self, ForgeFileError}, -}; +use crate::gitforge::{self, ForgeFileError}; pub async fn load( details: &RepoDetails, @@ -43,7 +42,7 @@ pub async fn validate( })?; if !branches .iter() - .any(|branch| branch.name() == &config.branches().main()) + .any(|branch| branch == &config.branches().main()) { return Err(RepoConfigValidationErrors::BranchNotFound( config.branches().main(), @@ -51,7 +50,7 @@ pub async fn validate( } if !branches .iter() - .any(|branch| branch.name() == &config.branches().next()) + .any(|branch| branch == &config.branches().next()) { return Err(RepoConfigValidationErrors::BranchNotFound( config.branches().next(), @@ -59,7 +58,7 @@ pub async fn validate( } if !branches .iter() - .any(|branch| branch.name() == &config.branches().dev()) + .any(|branch| branch == &config.branches().dev()) { return Err(RepoConfigValidationErrors::BranchNotFound( config.branches().dev(), diff --git a/crates/server/src/config/mod.rs b/crates/server/src/config/mod.rs index c1d0cc7..64b80f3 100644 --- a/crates/server/src/config/mod.rs +++ b/crates/server/src/config/mod.rs @@ -4,20 +4,15 @@ pub mod load; use std::{ collections::HashMap, - fmt::{Display, Formatter}, net::SocketAddr, - ops::Deref, path::{Path, PathBuf}, str::FromStr, }; -use serde::Deserialize; - +use git_next_config::{ForgeConfig, ForgeName}; use kxio::fs::FileSystem; use tracing::info; -use crate::{gitforge::Repository, types::ServerGeneration}; - #[derive(Debug, derive_more::From, derive_more::Display)] pub enum Error { Io(std::io::Error), @@ -30,7 +25,7 @@ impl std::error::Error for Error {} type Result = core::result::Result; /// Mapped from the `git-next-server.toml` file -#[derive(Debug, PartialEq, Eq, Deserialize, Message)] +#[derive(Debug, PartialEq, Eq, serde::Deserialize, Message)] #[rtype(result = "()")] pub struct ServerConfig { http: Http, @@ -67,7 +62,7 @@ impl ServerConfig { } /// Defines the port the server will listen to for incoming webhooks messages -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub struct Http { addr: String, port: u16, @@ -83,7 +78,7 @@ impl Http { /// Defines the Webhook Forges should send updates to /// Must be an address that is accessible from the remote forge -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub struct Webhook { url: String, } @@ -94,7 +89,7 @@ impl Webhook { } /// The URL for the webhook where forges should send their updates -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub struct WebhookUrl(String); impl AsRef for WebhookUrl { fn as_ref(&self) -> &str { @@ -103,7 +98,7 @@ impl AsRef for WebhookUrl { } /// The directory to store server data, such as cloned repos -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub struct ServerStorage { path: PathBuf, } @@ -113,533 +108,5 @@ impl ServerStorage { } } -/// 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, - source: RepoConfigSource, -} -impl RepoConfig { - #[cfg(test)] - pub const fn new(branches: RepoBranches, source: RepoConfigSource) -> Self { - Self { branches, source } - } - - pub fn load(toml: &str) -> Result { - toml::from_str(format!("source = \"Repo\"\n{}", toml).as_str()).map_err(Into::into) - } - - pub const fn branches(&self) -> &RepoBranches { - &self.branches - } - - pub const fn source(&self) -> RepoConfigSource { - self.source - } -} -impl Display for RepoConfig { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.branches) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] -pub enum RepoConfigSource { - Repo, - Server, -} - -/// 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.main, self.next, self.dev) - } -} - -/// 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.to_string().to_lowercase(), - 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 { - 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 { - 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 { - 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(), - }, - source: RepoConfigSource::Server, - }), - _ => 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) - } -} -impl From<&ForgeName> for PathBuf { - fn from(value: &ForgeName) -> Self { - Self::from(&value.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 generation: ServerGeneration, - 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( - generation: ServerGeneration, - repo_alias: &RepoAlias, - server_repo_config: &ServerRepoConfig, - forge_name: &ForgeName, - forge_config: &ForgeConfig, - gitdir: GitDir, - ) -> Self { - Self { - generation, - 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(), - 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() - } - - 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, - "gen-{}:{}:{}/{}:{}@{}/{}@{}", - self.generation, - self.forge.forge_type, - self.forge.forge_name, - self.repo_alias, - self.forge.user, - self.forge.hostname, - self.repo_path, - self.branch, - ) - } -} - -type ValidationResult = core::result::Result; -#[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}" - ), - } - } -} - -/// 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, "{}", format!("{:?}", self).to_lowercase()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub struct GitDir(PathBuf); -impl GitDir { - #[cfg(test)] - pub(crate) fn new(pathbuf: &std::path::Path) -> Self { - info!("GitDir::new({pathbuf:?})"); - Self(pathbuf.to_path_buf()) - } - - pub const fn pathbuf(&self) -> &PathBuf { - &self.0 - } - - #[tracing::instrument(skip_all)] - pub fn validate( - &self, - repository: &Repository, - repo_details: &RepoDetails, - ) -> ValidationResult<()> { - let git_remote = repo_details.git_remote(); - use gix::remote::Direction; - let push_remote = self.find_default_remote(repository, Direction::Push)?; - let fetch_remote = self.find_default_remote(repository, Direction::Fetch)?; - info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match"); - 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(skip_all, fields(?direction))] - fn find_default_remote( - &self, - repository: &Repository, - direction: gix::remote::Direction, - ) -> ValidationResult { - let repository = repository.deref().to_thread_local(); - 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()), - )) - } -} -impl Deref for GitDir { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &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()) - } -} -impl From<&GitDir> for PathBuf { - fn from(value: &GitDir) -> Self { - value.to_path_buf() - } -} -impl From for GitDir { - fn from(value: PathBuf) -> Self { - Self(value) - } -} -impl From 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) - } -} - #[cfg(test)] mod tests; diff --git a/crates/server/src/config/tests.rs b/crates/server/src/config/tests.rs index d669ad2..aef989e 100644 --- a/crates/server/src/config/tests.rs +++ b/crates/server/src/config/tests.rs @@ -1,10 +1,14 @@ use assert2::let_assert; +use git_next_config::{ + ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, + ServerRepoConfig, +}; +use git_next_git::{self as git, Generation, GitRemote, Repository}; use gix::remote::Direction; +use kxio::fs; use pretty_assertions::assert_eq; -use crate::{gitforge::tests::common /* server::gitforge::tests::common */}; - -use kxio::fs; +use crate::gitforge::tests::common; use super::*; @@ -163,11 +167,11 @@ fn gitdir_should_display_as_pathbuf() { // git.kemitix.net:kemitix/git-next // If the default push remote is something else, then this test will fail fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { - let cli_crate_dir = std::env::current_dir().map_err(RepoValidationError::Io)?; + let cli_crate_dir = std::env::current_dir().map_err(git::validate::Error::Io)?; let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent())); let mut repo_details = common::repo_details( 1, - ServerGeneration::new(), + Generation::new(), common::forge_details(1, ForgeType::MockForge), None, GitDir::new(root), // Server GitDir - should be ignored @@ -176,7 +180,7 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { repo_details.repo_path = RepoPath("kemitix/git-next".to_string()); let gitdir = &repo_details.gitdir; let repository = Repository::open(gitdir)?; - let found_git_remote = gitdir.find_default_remote(&repository, Direction::Push)?; + let found_git_remote = git::validate::find_default_remote(&repository, Direction::Push)?; let config_git_remote = repo_details.git_remote(); assert_eq!( @@ -189,11 +193,11 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { #[test] fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> { - let cli_crate_dir = std::env::current_dir().map_err(RepoValidationError::Io)?; + let cli_crate_dir = std::env::current_dir().map_err(git::validate::Error::Io)?; let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent())); let mut repo_details = common::repo_details( 1, - ServerGeneration::new(), + Generation::new(), common::forge_details(1, ForgeType::MockForge), None, GitDir::new(root), // Server GitDir - should be ignored @@ -202,18 +206,18 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> { repo_details.repo_path = RepoPath("kemitix/git-next".to_string()); let gitdir = &repo_details.gitdir; let repository = Repository::open(gitdir)?; - gitdir.validate(&repository, &repo_details)?; + git::validate(&repository, &repo_details)?; Ok(()) } #[test] fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> { - let_assert!(Ok(cli_crate_dir) = std::env::current_dir().map_err(RepoValidationError::Io)); + let_assert!(Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validate::Error::Io)); let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent())); let mut repo_details = common::repo_details( 1, - ServerGeneration::new(), + Generation::new(), common::forge_details(1, ForgeType::MockForge), None, GitDir::new(root), // Server GitDir - should be ignored @@ -222,7 +226,7 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> { repo_details.repo_path = RepoPath("hello/world".to_string()); let gitdir = &repo_details.gitdir; let repository = Repository::open(gitdir)?; - let_assert!(Err(_) = gitdir.validate(&repository, &repo_details)); + let_assert!(Err(_) = git::validate(&repository, &repo_details)); Ok(()) } diff --git a/crates/server/src/git/mod.rs b/crates/server/src/git/mod.rs deleted file mode 100644 index ce8a5bb..0000000 --- a/crates/server/src/git/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod reset; - -pub use reset::reset; diff --git a/crates/server/src/gitforge/errors.rs b/crates/server/src/gitforge/errors.rs index 0b11348..e8de262 100644 --- a/crates/server/src/gitforge/errors.rs +++ b/crates/server/src/gitforge/errors.rs @@ -1,4 +1,4 @@ -use crate::config::{BranchName, GitDir}; +use git_next_config::BranchName; #[derive(Debug)] pub enum ForgeFileError { @@ -39,45 +39,3 @@ impl std::fmt::Display for ForgeBranchError { } } } - -#[derive(Debug)] -pub enum RepoCloneError { - InvalidGitDir(GitDir), - Io(std::io::Error), - Wait(std::io::Error), - Spawn(std::io::Error), - Validation(String), - GixClone(Box), - GixOpen(Box), - GixFetch(Box), -} -impl std::error::Error for RepoCloneError {} -impl std::fmt::Display for RepoCloneError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidGitDir(gitdir) => write!(f, "Invalid Git dir: {:?}", gitdir), - Self::Io(err) => write!(f, "IO error: {:?}", err), - Self::Wait(err) => write!(f, "Waiting for command: {:?}", err), - Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err), - Self::Validation(err) => write!(f, "Validation: {}", err), - Self::GixClone(err) => write!(f, "Clone: {:?}", err), - Self::GixOpen(err) => write!(f, "Open: {:?}", err), - Self::GixFetch(err) => write!(f, "Fetch: {:?}", err), - } - } -} -impl From for RepoCloneError { - fn from(value: gix::clone::Error) -> Self { - Self::GixClone(Box::new(value)) - } -} -impl From for RepoCloneError { - fn from(value: gix::open::Error) -> Self { - Self::GixOpen(Box::new(value)) - } -} -impl From for RepoCloneError { - fn from(value: gix::clone::fetch::Error) -> Self { - Self::GixFetch(Box::new(value)) - } -} diff --git a/crates/server/src/gitforge/forgejo/branch/get_all.rs b/crates/server/src/gitforge/forgejo/branch/get_all.rs index 87445d3..648c6cb 100644 --- a/crates/server/src/gitforge/forgejo/branch/get_all.rs +++ b/crates/server/src/gitforge/forgejo/branch/get_all.rs @@ -1,15 +1,14 @@ +use git_next_config::BranchName; +use git_next_git::RepoDetails; use kxio::network::{self, Network}; use tracing::error; -use crate::{ - config::{BranchName, RepoDetails}, - gitforge::{self, ForgeBranchError}, -}; +use crate::gitforge::ForgeBranchError; pub async fn get_all( repo_details: &RepoDetails, net: &Network, -) -> Result, ForgeBranchError> { +) -> Result, ForgeBranchError> { let hostname = &repo_details.forge.hostname; let repo_path = &repo_details.repo_path; use secrecy::ExposeSecret; @@ -37,8 +36,7 @@ pub async fn get_all( .response_body() .unwrap_or_default() .into_iter() - .map(|b| b.name()) - .map(gitforge::Branch) + .map(BranchName::from) .collect::>(); Ok(branches) } @@ -47,8 +45,8 @@ pub async fn get_all( struct Branch { name: String, } -impl Branch { - fn name(&self) -> BranchName { - BranchName(self.name.clone()) +impl From for BranchName { + fn from(value: Branch) -> Self { + Self(value.name) } } diff --git a/crates/server/src/gitforge/forgejo/branch/mod.rs b/crates/server/src/gitforge/forgejo/branch/mod.rs index d756722..2216286 100644 --- a/crates/server/src/gitforge/forgejo/branch/mod.rs +++ b/crates/server/src/gitforge/forgejo/branch/mod.rs @@ -1,8 +1,6 @@ -pub mod fetch; mod get_all; mod validate_positions; -pub use fetch::fetch; pub use get_all::get_all; pub use validate_positions::validate_positions; diff --git a/crates/server/src/gitforge/forgejo/branch/validate_positions.rs b/crates/server/src/gitforge/forgejo/branch/validate_positions.rs index 2f42c57..190465c 100644 --- a/crates/server/src/gitforge/forgejo/branch/validate_positions.rs +++ b/crates/server/src/gitforge/forgejo/branch/validate_positions.rs @@ -1,10 +1,9 @@ +use git_next_config::{BranchName, RepoConfig}; +use git_next_git::{self as git, RepoDetails}; use kxio::network; use tracing::{debug, error, info, warn}; -use crate::{ - config::{BranchName, RepoConfig, RepoDetails}, - gitforge::{self, ForgeLike}, -}; +use crate::gitforge::{self, ForgeLike}; #[derive(Debug, derive_more::Display)] pub enum Error { @@ -12,7 +11,7 @@ pub enum Error { #[display("Failed to Reset Branch {branch} to {commit}")] FailedToResetBranch { branch: BranchName, - commit: gitforge::Commit, + commit: git::Commit, }, BranchReset(BranchName), BranchHasNoCommits(BranchName), @@ -21,15 +20,15 @@ pub enum Error { impl std::error::Error for Error {} pub struct ValidatedPositions { - pub main: gitforge::Commit, - pub next: gitforge::Commit, - pub dev: gitforge::Commit, - pub dev_commit_history: Vec, + pub main: git::Commit, + pub next: git::Commit, + pub dev: git::Commit, + pub dev_commit_history: Vec, } pub async fn validate_positions( forge: &gitforge::forgejo::ForgeJoEnv, - repository: &gitforge::Repository, + repository: &git::Repository, repo_config: RepoConfig, ) -> Result { let repo_details = &forge.repo_details; @@ -78,7 +77,7 @@ pub async fn validate_positions( repository, repo_config.branches().next(), main.into(), - gitforge::Force::From(next.clone().into()), + git::reset::Force::From(next.clone().into()), ) { warn!(?err, "Failed to reset next to main"); return Err(Error::FailedToResetBranch { @@ -104,7 +103,7 @@ pub async fn validate_positions( repository, repo_config.branches().next(), main.into(), - gitforge::Force::From(next.clone().into()), + git::reset::Force::From(next.clone().into()), ) { warn!(?err, "Failed to reset next to main"); return Err(Error::FailedToResetBranch { @@ -186,11 +185,11 @@ async fn get_commit_histories( #[tracing::instrument(fields(%branch_name),skip_all)] async fn get_commit_history( - repo_details: &crate::config::RepoDetails, + repo_details: &RepoDetails, branch_name: &BranchName, - find_commits: Vec, + find_commits: Vec, net: &kxio::network::Network, -) -> Result, network::NetworkError> { +) -> Result, network::NetworkError> { let hostname = &repo_details.forge.hostname; let repo_path = &repo_details.repo_path; @@ -223,7 +222,7 @@ async fn get_commit_history( .response_body() .unwrap_or_default() .into_iter() - .map(gitforge::Commit::from) + .map(git::Commit::from) .collect::>(); let found = find_commits.is_empty() @@ -253,7 +252,7 @@ struct Commit { struct RepoCommit { message: String, } -impl From for gitforge::Commit { +impl From for git::Commit { fn from(value: Commit) -> Self { Self::new(&value.sha, &value.commit.message) } diff --git a/crates/server/src/gitforge/forgejo/file/mod.rs b/crates/server/src/gitforge/forgejo/file/mod.rs index dbf38c0..94beaae 100644 --- a/crates/server/src/gitforge/forgejo/file/mod.rs +++ b/crates/server/src/gitforge/forgejo/file/mod.rs @@ -1,10 +1,9 @@ +use git_next_config::BranchName; +use git_next_git::RepoDetails; use kxio::network::{self, Network}; use tracing::{error, warn}; -use crate::{ - config::{BranchName, RepoDetails}, - gitforge::ForgeFileError, -}; +use crate::gitforge::ForgeFileError; pub(super) async fn contents_get( repo_details: &RepoDetails, diff --git a/crates/server/src/gitforge/forgejo/mod.rs b/crates/server/src/gitforge/forgejo/mod.rs index 4f9535d..eb3765c 100644 --- a/crates/server/src/gitforge/forgejo/mod.rs +++ b/crates/server/src/gitforge/forgejo/mod.rs @@ -5,15 +5,15 @@ use std::time::Duration; use actix::prelude::*; +use git_next_config::{BranchName, GitDir, RepoConfig}; +use git_next_git::{self as git, GitRef, RepoDetails, Repository}; use kxio::network::{self, Network}; use tracing::{error, info, warn}; use crate::{ actors::repo::{RepoActor, StartMonitoring, ValidateRepo}, - config::{BranchName, GitDir, RepoConfig, RepoDetails}, - git, - gitforge::{self, forgejo::branch::ValidatedPositions, RepoCloneError, Repository}, - types::{GitRef, MessageToken}, + gitforge::{self, forgejo::branch::ValidatedPositions}, + types::MessageToken, }; struct ForgeJo; @@ -33,7 +33,7 @@ impl super::ForgeLike for ForgeJoEnv { "forgejo".to_string() } - async fn branches_get_all(&self) -> Result, gitforge::ForgeBranchError> { + async fn branches_get_all(&self) -> Result, gitforge::ForgeBranchError> { branch::get_all(&self.repo_details, &self.net).await } @@ -47,7 +47,7 @@ impl super::ForgeLike for ForgeJoEnv { async fn branches_validate_positions( &self, - repository: Repository, + repository: git::Repository, repo_config: RepoConfig, addr: Addr, message_token: MessageToken, @@ -76,12 +76,12 @@ impl super::ForgeLike for ForgeJoEnv { fn branch_reset( &self, - repository: &Repository, + repository: &git::Repository, branch_name: BranchName, to_commit: GitRef, - force: gitforge::Force, - ) -> gitforge::BranchResetResult { - branch::fetch(repository, &self.repo_details)?; + force: git::reset::Force, + ) -> git::reset::Result { + git::fetch(repository, &self.repo_details)?; git::reset( repository, &self.repo_details, @@ -91,7 +91,7 @@ impl super::ForgeLike for ForgeJoEnv { ) } - async fn commit_status(&self, commit: &gitforge::Commit) -> gitforge::CommitStatus { + async fn commit_status(&self, commit: &git::Commit) -> gitforge::CommitStatus { let repo_details = &self.repo_details; let hostname = &repo_details.forge.hostname; let repo_path = &repo_details.repo_path; @@ -135,17 +135,16 @@ impl super::ForgeLike for ForgeJoEnv { } } - fn repo_clone(&self, gitdir: GitDir) -> Result { + fn repo_clone(&self, gitdir: GitDir) -> Result { let repository = if !gitdir.exists() { info!("Local copy not found - cloning..."); Repository::clone(&self.repo_details)? } else { - Repository::open(gitdir.clone())? + Repository::open(gitdir)? }; info!("Validating..."); - gitdir - .validate(&repository, &self.repo_details) - .map_err(|e| RepoCloneError::Validation(e.to_string())) + git::validate(&repository, &self.repo_details) + .map_err(|e| git::repository::Error::Validation(e.to_string())) .inspect(|_| info!("Validation - OK"))?; Ok(repository) } diff --git a/crates/server/src/gitforge/mock_forge.rs b/crates/server/src/gitforge/mock_forge.rs index 3803a55..e3ab3f5 100644 --- a/crates/server/src/gitforge/mock_forge.rs +++ b/crates/server/src/gitforge/mock_forge.rs @@ -1,9 +1,7 @@ -use crate::{ - actors::repo::RepoActor, - config::{BranchName, GitDir, RepoConfig}, - gitforge::{self, RepoCloneError, Repository}, - types::{GitRef, MessageToken}, -}; +use git_next_config::{BranchName, GitDir, RepoConfig}; +use git_next_git::{self as git, GitRef, Repository}; + +use crate::{actors::repo::RepoActor, gitforge, types::MessageToken}; struct MockForge; #[derive(Clone, Debug)] @@ -19,7 +17,7 @@ impl super::ForgeLike for MockForgeEnv { "mock".to_string() } - async fn branches_get_all(&self) -> Result, gitforge::ForgeBranchError> { + async fn branches_get_all(&self) -> Result, gitforge::ForgeBranchError> { todo!() } @@ -46,16 +44,16 @@ impl super::ForgeLike for MockForgeEnv { _repository: &Repository, _branch_name: BranchName, _to_commit: GitRef, - _force: gitforge::Force, - ) -> gitforge::BranchResetResult { + _force: git::reset::Force, + ) -> git::reset::Result { todo!() } - async fn commit_status(&self, _commit: &gitforge::Commit) -> gitforge::CommitStatus { + async fn commit_status(&self, _commit: &git::Commit) -> gitforge::CommitStatus { todo!() } - fn repo_clone(&self, _gitdir: GitDir) -> Result { + fn repo_clone(&self, _gitdir: GitDir) -> Result { todo!() } } diff --git a/crates/server/src/gitforge/mod.rs b/crates/server/src/gitforge/mod.rs index 9302877..c563038 100644 --- a/crates/server/src/gitforge/mod.rs +++ b/crates/server/src/gitforge/mod.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +use git_next_config::{BranchName, GitDir, RepoConfig}; +use git_next_git::{self as git, GitRef, RepoDetails, Repository}; use kxio::network::Network; #[cfg(feature = "forgejo")] @@ -16,22 +18,19 @@ pub use types::*; mod errors; pub use errors::*; -use crate::{ - config::{BranchName, GitDir, RepoConfig, RepoDetails}, - types::{GitRef, MessageToken}, -}; +use crate::types::MessageToken; #[async_trait::async_trait] pub trait ForgeLike { fn name(&self) -> String; /// Returns a list of all branches in the repo. - async fn branches_get_all(&self) -> Result, ForgeBranchError>; + async fn branches_get_all(&self) -> Result, ForgeBranchError>; /// Returns the contents of the file. async fn file_contents_get( &self, - branch: &super::config::BranchName, + branch: &BranchName, file_path: &str, ) -> Result; @@ -51,14 +50,14 @@ pub trait ForgeLike { repository: &Repository, branch_name: BranchName, to_commit: GitRef, - force: Force, - ) -> BranchResetResult; + force: git::reset::Force, + ) -> git::reset::Result; /// Checks the results of any (e.g. CI) status checks for the commit. - async fn commit_status(&self, commit: &Commit) -> CommitStatus; + async fn commit_status(&self, commit: &git::Commit) -> CommitStatus; /// Clones a repo to disk. - fn repo_clone(&self, gitdir: GitDir) -> Result; + fn repo_clone(&self, gitdir: GitDir) -> Result; } #[derive(Clone, Debug)] diff --git a/crates/server/src/gitforge/tests/common.rs b/crates/server/src/gitforge/tests/common.rs index b456892..b55b5d2 100644 --- a/crates/server/src/gitforge/tests/common.rs +++ b/crates/server/src/gitforge/tests/common.rs @@ -1,11 +1,10 @@ -use crate::{ - config::{ - ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, GitDir, Hostname, RepoAlias, - RepoBranches, RepoConfig, RepoConfigSource, RepoDetails, RepoPath, User, - }, - types::ServerGeneration, +use git_next_config::{ + ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, GitDir, Hostname, RepoAlias, + RepoBranches, RepoConfig, RepoConfigSource, RepoPath, User, }; +use git_next_git::{Generation, RepoDetails}; + pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails { ForgeDetails { forge_name: forge_name(n), @@ -33,7 +32,7 @@ pub fn forge_name(n: u32) -> ForgeName { } pub fn repo_details( n: u32, - generation: ServerGeneration, + generation: Generation, forge: ForgeDetails, repo_config: Option, gitdir: GitDir, diff --git a/crates/server/src/gitforge/tests/forgejo.rs b/crates/server/src/gitforge/tests/forgejo.rs index 232dbfe..b331544 100644 --- a/crates/server/src/gitforge/tests/forgejo.rs +++ b/crates/server/src/gitforge/tests/forgejo.rs @@ -1,11 +1,9 @@ use assert2::let_assert; +use git_next_config::{ForgeType, RepoConfigSource}; use kxio::network::{MockNetwork, StatusCode}; -use crate::{ - config::{BranchName, ForgeType, RepoConfigSource}, - types::ServerGeneration, -}; +use git_next_git::Generation; use super::*; @@ -17,7 +15,7 @@ fn test_name() { let net = Network::new_mock(); let repo_details = common::repo_details( 1, - ServerGeneration::new(), + Generation::new(), common::forge_details(1, ForgeType::MockForge), Some(common::repo_config(1, RepoConfigSource::Repo)), GitDir::new(fs.base()), @@ -44,7 +42,7 @@ async fn test_branches_get() { let repo_details = common::repo_details( 1, - ServerGeneration::new(), + Generation::new(), common::forge_details(1, ForgeType::MockForge), Some(common::repo_config(1, RepoConfigSource::Repo)), GitDir::new(fs.base()), @@ -57,5 +55,5 @@ async fn test_branches_get() { let_assert!(Some(requests) = net.mocked_requests()); assert_eq!(requests.len(), 1); - assert_eq!(branches, vec![Branch(BranchName("string".into()))]); + assert_eq!(branches, vec![BranchName("string".into())]); } diff --git a/crates/server/src/gitforge/types.rs b/crates/server/src/gitforge/types.rs index cc1edf6..6bb62e2 100644 --- a/crates/server/src/gitforge/types.rs +++ b/crates/server/src/gitforge/types.rs @@ -1,41 +1,4 @@ -use std::{ops::Deref as _, path::PathBuf, sync::atomic::AtomicBool}; - -use crate::{ - config::{BranchName, RepoDetails}, - gitforge::RepoCloneError, -}; - -#[derive(Debug, PartialEq, Eq)] -pub struct Branch(pub BranchName); -impl Branch { - pub const fn name(&self) -> &BranchName { - &self.0 - } -} - -#[derive(Debug)] -pub enum Force { - No, - From(crate::types::GitRef), -} -impl std::fmt::Display for Force { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::No => write!(f, "fast-foward"), - Self::From(from) => write!(f, "force-if-from:{}", from), - } - } -} - -#[derive(Debug, derive_more::From, derive_more::Display)] -pub enum BranchResetError { - Open(Box), - Fetch(crate::gitforge::forgejo::branch::fetch::Error), - Push, -} -impl std::error::Error for BranchResetError {} - -pub type BranchResetResult = Result<(), BranchResetError>; +use git_next_git as git; #[derive(Debug)] pub enum CommitStatus { @@ -46,82 +9,7 @@ pub enum CommitStatus { #[derive(Clone, Debug)] pub struct CommitHistories { - pub main: Vec, - pub next: Vec, - pub dev: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Commit { - sha: Sha, - message: Message, -} -impl Commit { - pub fn new(sha: &str, message: &str) -> Self { - Self { - sha: Sha::new(sha.to_string()), - message: Message::new(message.to_string()), - } - } - pub const fn sha(&self) -> &Sha { - &self.sha - } - pub const fn message(&self) -> &Message { - &self.message - } -} -impl std::fmt::Display for Commit { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.sha) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Sha(String); -impl Sha { - pub const fn new(value: String) -> Self { - Self(value) - } -} -impl std::fmt::Display for Sha { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Message(String); -impl Message { - pub const fn new(value: String) -> Self { - Self(value) - } -} -impl std::fmt::Display for Message { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, derive_more::From)] -pub struct Repository(gix::ThreadSafeRepository); -impl Repository { - pub fn open(gitdir: impl Into) -> Result { - Ok(Self(gix::ThreadSafeRepository::open(gitdir.into())?)) - } - pub fn clone(repo_details: &RepoDetails) -> Result { - use secrecy::ExposeSecret; - let origin = repo_details.origin(); - let (repository, _outcome) = - gix::prepare_clone_bare(origin.expose_secret().as_str(), repo_details.gitdir.deref())? - .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; - - Ok(repository.into_sync().into()) - } -} -impl std::ops::Deref for Repository { - type Target = gix::ThreadSafeRepository; - - fn deref(&self) -> &Self::Target { - &self.0 - } + pub main: Vec, + pub next: Vec, + pub dev: Vec, } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 9679666..3dc0a72 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,6 +1,5 @@ mod actors; mod config; -pub mod git; pub mod gitforge; pub mod types; diff --git a/crates/server/src/types.rs b/crates/server/src/types.rs index fa4a4e3..49b6725 100644 --- a/crates/server/src/types.rs +++ b/crates/server/src/types.rs @@ -1,25 +1,3 @@ -use std::fmt::Display; - -use crate::{config::BranchName, gitforge}; - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct GitRef(pub String); -impl From for GitRef { - fn from(value: gitforge::Commit) -> Self { - Self(value.sha().to_string()) - } -} -impl From for GitRef { - fn from(value: BranchName) -> Self { - Self(value.0) - } -} -impl Display for GitRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct MessageToken(u32); impl MessageToken { @@ -35,19 +13,3 @@ impl std::fmt::Display for MessageToken { write!(f, "{}", self.0) } } - -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct ServerGeneration(u32); -impl ServerGeneration { - pub fn new() -> Self { - Self::default() - } - pub fn inc(&mut self) { - self.0 += 1 - } -} -impl std::fmt::Display for ServerGeneration { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -}