diff --git a/Cargo.toml b/Cargo.toml index f97572e..7c427be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,13 @@ [workspace] resolver = "2" -members = ["crates/cli", "crates/server", "crates/config", "crates/git"] +members = [ + "crates/cli", + "crates/server", + "crates/config", + "crates/git", + "crates/gitforge", + "crates/repo-actor", +] [workspace.package] version = "0.5.1" @@ -16,6 +23,8 @@ expect_used = "warn" git-next-server = { path = "crates/server" } git-next-config = { path = "crates/config" } git-next-git = { path = "crates/git" } +git-next-gitforge = { path = "crates/gitforge" } +git-next-repo-actor = { path = "crates/repo-actor" } # CLI parsing clap = { version = "4.5", features = ["cargo", "derive"] } diff --git a/README.md b/README.md index bde3241..de3ea25 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,22 @@ The following diagram shows the dependency between the crates that make up `git- stateDiagram-v2 cli --> server cli --> git + server --> config server --> git + server --> gitforge + server --> repo_actor + git --> config + + gitforge --> config + gitforge --> git + + repo_actor --> config + repo_actor --> git + repo_actor --> gitforge + + ``` ## License diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 53f7463..30d5810 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -9,9 +9,9 @@ forgejo = [] github = [] [dependencies] -# # logging +# logging # console-subscriber = { workspace = true } -# tracing = { workspace = true } +tracing = { workspace = true } # tracing-subscriber = { workspace = true } # # base64 decoding @@ -21,9 +21,9 @@ github = [] # # gix = { workspace = true } # gix = { workspace = true } # async-trait = { workspace = true } -# -# # fs/network -# kxio = { workspace = true } + +# fs/network +kxio = { workspace = true } # TOML parsing serde = { workspace = true } @@ -47,12 +47,12 @@ derive-with = { workspace = true } # # # file watcher # inotify = { workspace = true } -# -# # Actors -# actix = { workspace = true } + +# Actors +actix = { workspace = true } # actix-rt = { workspace = true } # tokio = { workspace = true } -# + [dev-dependencies] # # Testing assert2 = { workspace = true } diff --git a/crates/server/src/gitforge/tests/common.rs b/crates/config/src/common.rs similarity index 63% rename from crates/server/src/gitforge/tests/common.rs rename to crates/config/src/common.rs index 8354708..82375f3 100644 --- a/crates/server/src/gitforge/tests/common.rs +++ b/crates/config/src/common.rs @@ -1,10 +1,8 @@ -use git_next_config::{ - ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, GitDir, Hostname, RepoAlias, - RepoBranches, RepoConfig, RepoConfigSource, RepoPath, User, +use crate::{ + ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, Hostname, RepoAlias, RepoBranches, + RepoConfig, RepoConfigSource, RepoPath, User, }; -use git_next_git::{Generation, RepoDetails}; - pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails { ForgeDetails::new( forge_name(n), @@ -30,23 +28,6 @@ pub fn hostname(n: u32) -> Hostname { pub fn forge_name(n: u32) -> ForgeName { ForgeName::new(format!("forge-name-{}", n)) } -pub fn repo_details( - n: u32, - generation: Generation, - forge: ForgeDetails, - repo_config: Option, - gitdir: GitDir, -) -> RepoDetails { - RepoDetails { - generation, - repo_alias: repo_alias(n), - repo_path: repo_path(n), - gitdir, - branch: branch_name(n), - forge, - repo_config, - } -} pub fn branch_name(n: u32) -> BranchName { BranchName::new(format!("branch-name-{}", n)) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index bda164a..133bcda 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,6 +1,7 @@ // mod api_token; mod branch_name; +pub mod common; mod forge_config; mod forge_details; mod forge_name; @@ -12,6 +13,7 @@ mod repo_branches; mod repo_config; mod repo_config_source; mod repo_path; +pub mod server; mod server_repo_config; mod user; diff --git a/crates/config/src/server.rs b/crates/config/src/server.rs new file mode 100644 index 0000000..f3e3394 --- /dev/null +++ b/crates/config/src/server.rs @@ -0,0 +1,105 @@ +// + +use actix::prelude::*; + +use std::{ + collections::HashMap, + net::SocketAddr, + path::{Path, PathBuf}, + str::FromStr, +}; + +use kxio::fs::FileSystem; +use tracing::info; + +use crate::{ForgeConfig, ForgeName}; + +#[derive(Debug, derive_more::From, derive_more::Display)] +pub enum Error { + Io(std::io::Error), + KxIoFs(kxio::fs::Error), + TomlDe(toml::de::Error), + AddressParse(std::net::AddrParseError), +} +impl std::error::Error for Error {} + +type Result = core::result::Result; + +/// Mapped from the `git-next-server.toml` file +#[derive(Debug, PartialEq, Eq, serde::Deserialize, Message, derive_more::Constructor)] +#[rtype(result = "()")] +pub struct ServerConfig { + http: Http, + webhook: Webhook, + storage: ServerStorage, + pub forge: HashMap, +} +impl ServerConfig { + #[tracing::instrument(skip_all)] + pub fn load(fs: &FileSystem) -> Result { + let file = fs.base().join("git-next-server.toml"); + info!(?file, ""); + let str = fs.file_read_to_string(&file)?; + toml::from_str(&str).map_err(Into::into) + } + + pub fn forges(&self) -> impl Iterator { + self.forge + .iter() + .map(|(name, forge)| (ForgeName::new(name.clone()), forge)) + } + + pub const fn storage(&self) -> &ServerStorage { + &self.storage + } + + pub const fn webhook(&self) -> &Webhook { + &self.webhook + } + + pub fn http(&self) -> Result { + self.http.socket_addr() + } +} + +/// Defines the port the server will listen to for incoming webhooks messages +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::Constructor)] +pub struct Http { + addr: String, + port: u16, +} +impl Http { + fn socket_addr(&self) -> Result { + Ok(SocketAddr::from_str(&format!( + "{}:{}", + self.addr, self.port + ))?) + } +} + +/// Defines the Webhook Forges should send updates to +/// Must be an address that is accessible from the remote forge +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::Constructor)] +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, serde::Deserialize, derive_more::AsRef)] +pub struct WebhookUrl(String); + +/// The directory to store server data, such as cloned repos +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::Constructor)] +pub struct ServerStorage { + path: PathBuf, +} +impl ServerStorage { + pub fn path(&self) -> &Path { + self.path.as_path() + } +} diff --git a/crates/config/src/tests.rs b/crates/config/src/tests/mod.rs similarity index 75% rename from crates/config/src/tests.rs rename to crates/config/src/tests/mod.rs index aeef277..263dd16 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests/mod.rs @@ -1,4 +1,7 @@ -type TestResult = Result<(), Box>; +// + +type Result = core::result::Result>; +type TestResult = Result<()>; mod server_repo_config { use std::path::PathBuf; @@ -452,3 +455,123 @@ mod repo_branches { assert_eq!(repo_branches.dev(), BranchName::new("dev")); } } +mod server { + mod load { + // + + use std::collections::{BTreeMap, HashMap}; + + use assert2::let_assert; + + use crate::{ + server::{Http, ServerConfig, ServerStorage, Webhook}, + tests::TestResult, + ForgeConfig, ForgeType, RepoBranches, RepoConfig, RepoConfigSource, ServerRepoConfig, + }; + + #[test] + fn load_should_parse_server_config() -> TestResult { + let fs = kxio::fs::temp()?; + fs.file_write( + &fs.base().join("git-next-server.toml"), + r#" + [http] + addr = "0.0.0.0" + port = 8080 + + [webhook] + url = "http://localhost:9909/webhook" + + [storage] + path = "/opt/git-next/data" + + [forge.default] + forge_type = "MockForge" + hostname = "git.example.net" + user = "Bob" + token = "API-Token" + + [forge.default.repos] + hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/user/hello.git" } + world = { repo = "user/world", branch = "master", main = "main", next = "next", dev = "dev" } + + [forge.default.repos.sam] + repo = "user/sam" + branch = "main" + main = "master" + next = "upcoming" + dev = "sam-dev" + "#, + ) + ?; + let_assert!(Ok(config) = ServerConfig::load(&fs)); + let expected = ServerConfig::new( + Http::new("0.0.0.0".to_string(), 8080), + Webhook::new("http://localhost:9909/webhook".to_string()), + ServerStorage::new("/opt/git-next/data".into()), + HashMap::from([( + "default".to_string(), + ForgeConfig::new( + ForgeType::MockForge, + "git.example.net".to_string(), + "Bob".to_string(), + "API-Token".to_string(), + BTreeMap::from([ + ( + "hello".to_string(), + ServerRepoConfig::new( + "user/hello".to_string(), + "main".to_string(), + Some("/opt/git/user/hello.git".into()), + None, + None, + None, + ), + ), + ( + "world".to_string(), + ServerRepoConfig::new( + "user/world".to_string(), + "master".to_string(), + None, + Some("main".to_string()), + Some("next".to_string()), + Some("dev".to_string()), + ), + ), + ( + "sam".to_string(), + ServerRepoConfig::new( + "user/sam".to_string(), + "main".to_string(), + None, + Some("master".to_string()), + Some("upcoming".to_string()), + Some("sam-dev".to_string()), + ), + ), + ]), + ), + )]), + ); + assert_eq!(config, expected, "ServerConfig"); + + if let Some(forge) = config.forge.get("world") { + if let Some(repo) = forge.get_repo("sam") { + let repo_config = repo.repo_config(); + let expected = Some(RepoConfig::new( + RepoBranches::new( + "master".to_string(), + "upcoming".to_string(), + "sam-dev".to_string(), + ), + RepoConfigSource::Server, + )); + assert_eq!(repo_config, expected, "RepoConfig"); + } + } + + Ok(()) + } + } +} diff --git a/crates/git/src/common.rs b/crates/git/src/common.rs new file mode 100644 index 0000000..57fd59c --- /dev/null +++ b/crates/git/src/common.rs @@ -0,0 +1,26 @@ +// + +use git_next_config::{ + common::{branch_name, repo_alias, repo_path}, + ForgeDetails, GitDir, RepoConfig, +}; + +use crate::{Generation, RepoDetails}; + +pub fn repo_details( + n: u32, + generation: Generation, + forge: ForgeDetails, + repo_config: Option, + gitdir: GitDir, +) -> RepoDetails { + RepoDetails { + generation, + repo_alias: repo_alias(n), + repo_path: repo_path(n), + gitdir, + branch: branch_name(n), + forge, + repo_config, + } +} diff --git a/crates/git/src/lib.rs b/crates/git/src/lib.rs index e8f61fa..f6f1861 100644 --- a/crates/git/src/lib.rs +++ b/crates/git/src/lib.rs @@ -1,5 +1,6 @@ // pub mod commit; +pub mod common; pub mod fetch; mod generation; mod git_ref; diff --git a/crates/gitforge/Cargo.toml b/crates/gitforge/Cargo.toml new file mode 100644 index 0000000..941c768 --- /dev/null +++ b/crates/gitforge/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "git-next-gitforge" +version = { workspace = true } +edition = { workspace = true } + +[features] +default = ["forgejo"] +forgejo = [] +github = [] + +[dependencies] +git-next-config = { workspace = true } +git-next-git = { workspace = true } + +# logging +console-subscriber = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +# base64 decoding +base64 = { workspace = true } + +# git +async-trait = { workspace = true } + +# fs/network +kxio = { 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 } + +# boilerplate +derive_more = { workspace = true } + +# file watcher +inotify = { workspace = true } + +# # Actors +# actix = { workspace = true } +# actix-rt = { workspace = true } +tokio = { workspace = true } + +[dev-dependencies] +# Testing +assert2 = { workspace = true } + +[lints.clippy] +nursery = { level = "warn", priority = -1 } +# pedantic = "warn" +unwrap_used = "warn" +expect_used = "warn" diff --git a/crates/server/src/gitforge/errors.rs b/crates/gitforge/src/errors.rs similarity index 100% rename from crates/server/src/gitforge/errors.rs rename to crates/gitforge/src/errors.rs diff --git a/crates/server/src/gitforge/forgejo/branch/get_all.rs b/crates/gitforge/src/forgejo/branch/get_all.rs similarity index 97% rename from crates/server/src/gitforge/forgejo/branch/get_all.rs rename to crates/gitforge/src/forgejo/branch/get_all.rs index 9fa9028..1ff449d 100644 --- a/crates/server/src/gitforge/forgejo/branch/get_all.rs +++ b/crates/gitforge/src/forgejo/branch/get_all.rs @@ -3,7 +3,7 @@ use git_next_git::RepoDetails; use kxio::network::{self, Network}; use tracing::error; -use crate::gitforge::ForgeBranchError; +use crate::ForgeBranchError; pub async fn get_all( repo_details: &RepoDetails, diff --git a/crates/server/src/gitforge/forgejo/branch/mod.rs b/crates/gitforge/src/forgejo/branch/mod.rs similarity index 70% rename from crates/server/src/gitforge/forgejo/branch/mod.rs rename to crates/gitforge/src/forgejo/branch/mod.rs index 2216286..0685d50 100644 --- a/crates/server/src/gitforge/forgejo/branch/mod.rs +++ b/crates/gitforge/src/forgejo/branch/mod.rs @@ -4,4 +4,3 @@ mod validate_positions; pub use get_all::get_all; pub use validate_positions::validate_positions; -pub use validate_positions::ValidatedPositions; diff --git a/crates/server/src/gitforge/forgejo/branch/validate_positions.rs b/crates/gitforge/src/forgejo/branch/validate_positions.rs similarity index 80% rename from crates/server/src/gitforge/forgejo/branch/validate_positions.rs rename to crates/gitforge/src/forgejo/branch/validate_positions.rs index 9facb33..ae8cde0 100644 --- a/crates/server/src/gitforge/forgejo/branch/validate_positions.rs +++ b/crates/gitforge/src/forgejo/branch/validate_positions.rs @@ -3,34 +3,13 @@ use git_next_git::{self as git, RepoDetails}; use kxio::network; use tracing::{debug, error, info, warn}; -use crate::gitforge::{self, ForgeLike}; - -#[derive(Debug, derive_more::Display)] -pub enum Error { - Network(Box), - #[display("Failed to Reset Branch {branch} to {commit}")] - FailedToResetBranch { - branch: BranchName, - commit: git::Commit, - }, - BranchReset(BranchName), - BranchHasNoCommits(BranchName), - DevBranchNotBasedOn(BranchName), -} -impl std::error::Error for Error {} - -pub struct ValidatedPositions { - pub main: git::Commit, - pub next: git::Commit, - pub dev: git::Commit, - pub dev_commit_history: Vec, -} +use crate::{forgejo::ForgeJoEnv, validation, CommitHistories, ForgeLike as _}; pub async fn validate_positions( - forge: &gitforge::forgejo::ForgeJoEnv, + forge: &ForgeJoEnv, repository: &git::OpenRepository, repo_config: RepoConfig, -) -> Result { +) -> validation::Result { let repo_details = &forge.repo_details; // Collect Commit Histories for `main`, `next` and `dev` branches let commit_histories = get_commit_histories(repo_details, &repo_config, &forge.net).await; @@ -38,7 +17,7 @@ pub async fn validate_positions( Ok(commit_histories) => commit_histories, Err(err) => { error!(?err, "Failed to get commit histories"); - return Err(Error::Network(Box::new(err))); + return Err(validation::Error::Network(Box::new(err))); } }; @@ -48,7 +27,9 @@ pub async fn validate_positions( "No commits on main branch '{}'", repo_config.branches().main() ); - return Err(Error::BranchHasNoCommits(repo_config.branches().main())); + return Err(validation::Error::BranchHasNoCommits( + repo_config.branches().main(), + )); }; // Dev must be on main branch, or user must rebase it let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main); @@ -59,7 +40,9 @@ pub async fn validate_positions( repo_config.branches().main(), repo_config.branches().main(), ); - return Err(Error::DevBranchNotBasedOn(repo_config.branches().main())); + return Err(validation::Error::DevBranchNotBasedOn( + repo_config.branches().main(), + )); } // verify that next is an ancestor of dev, and force update to it main if it isn't let Some(next) = commit_histories.next.first().cloned() else { @@ -67,7 +50,9 @@ pub async fn validate_positions( "No commits on next branch '{}", repo_config.branches().next() ); - return Err(Error::BranchHasNoCommits(repo_config.branches().next())); + return Err(validation::Error::BranchHasNoCommits( + repo_config.branches().next(), + )); }; let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next); if !next_is_ancestor_of_dev { @@ -80,12 +65,14 @@ pub async fn validate_positions( git::push::Force::From(next.clone().into()), ) { warn!(?err, "Failed to reset next to main"); - return Err(Error::FailedToResetBranch { + return Err(validation::Error::FailedToResetBranch { branch: repo_config.branches().next(), commit: next, }); } - return Err(Error::BranchReset(repo_config.branches().next())); + return Err(validation::Error::BranchReset( + repo_config.branches().next(), + )); } let next_commits = commit_histories @@ -106,19 +93,23 @@ pub async fn validate_positions( git::push::Force::From(next.clone().into()), ) { warn!(?err, "Failed to reset next to main"); - return Err(Error::FailedToResetBranch { + return Err(validation::Error::FailedToResetBranch { branch: repo_config.branches().next(), commit: next, }); } - return Err(Error::BranchReset(repo_config.branches().next())); + return Err(validation::Error::BranchReset( + repo_config.branches().next(), + )); } let Some(next) = next_commits.first().cloned() else { warn!( "No commits on next branch '{}'", repo_config.branches().next() ); - return Err(Error::BranchHasNoCommits(repo_config.branches().next())); + return Err(validation::Error::BranchHasNoCommits( + repo_config.branches().next(), + )); }; let dev_has_next = commit_histories .dev @@ -130,7 +121,9 @@ pub async fn validate_positions( repo_config.branches().dev(), repo_config.branches().next() ); - return Err(Error::DevBranchNotBasedOn(repo_config.branches().next())); // dev is not based on next + return Err(validation::Error::DevBranchNotBasedOn( + repo_config.branches().next(), + )); // dev is not based on next } let Some(dev) = commit_histories.dev.first().cloned() else { @@ -138,10 +131,12 @@ pub async fn validate_positions( "No commits on dev branch '{}'", repo_config.branches().dev() ); - return Err(Error::BranchHasNoCommits(repo_config.branches().dev())); + return Err(validation::Error::BranchHasNoCommits( + repo_config.branches().dev(), + )); }; - Ok(ValidatedPositions { + Ok(validation::Positions { main, next, dev, @@ -153,7 +148,7 @@ async fn get_commit_histories( repo_details: &RepoDetails, repo_config: &RepoConfig, net: &network::Network, -) -> Result { +) -> Result { let main = (get_commit_history(repo_details, &repo_config.branches().main(), vec![], net).await)?; let main_head = main[0].clone(); @@ -178,7 +173,7 @@ async fn get_commit_histories( dev = dev.len(), "Commit histories" ); - let histories = gitforge::CommitHistories { main, next, dev }; + let histories = CommitHistories { main, next, dev }; Ok(histories) } diff --git a/crates/server/src/gitforge/forgejo/file/mod.rs b/crates/gitforge/src/forgejo/file.rs similarity index 98% rename from crates/server/src/gitforge/forgejo/file/mod.rs rename to crates/gitforge/src/forgejo/file.rs index 874dbf5..a199a82 100644 --- a/crates/server/src/gitforge/forgejo/file/mod.rs +++ b/crates/gitforge/src/forgejo/file.rs @@ -3,7 +3,7 @@ use git_next_git::RepoDetails; use kxio::network::{self, Network}; use tracing::{error, warn}; -use crate::gitforge::ForgeFileError; +use crate::ForgeFileError; pub(super) async fn contents_get( repo_details: &RepoDetails, diff --git a/crates/server/src/gitforge/forgejo/mod.rs b/crates/gitforge/src/forgejo/mod.rs similarity index 69% rename from crates/server/src/gitforge/forgejo/mod.rs rename to crates/gitforge/src/forgejo/mod.rs index 5d1de71..b0e4dc5 100644 --- a/crates/server/src/gitforge/forgejo/mod.rs +++ b/crates/gitforge/src/forgejo/mod.rs @@ -1,20 +1,13 @@ pub mod branch; mod file; -use std::time::Duration; - -use actix::prelude::*; - use git::OpenRepository; 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}, - gitforge, -}; +use crate::{validation, CommitStatus, ForgeBranchError, ForgeFileError}; struct ForgeJo; #[derive(Clone, Debug)] @@ -38,7 +31,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, ForgeBranchError> { branch::get_all(&self.repo_details, &self.net).await } @@ -46,7 +39,7 @@ impl super::ForgeLike for ForgeJoEnv { &self, branch: &BranchName, file_path: &str, - ) -> Result { + ) -> Result { file::contents_get(&self.repo_details, &self.net, branch, file_path).await } @@ -54,24 +47,8 @@ impl super::ForgeLike for ForgeJoEnv { &self, repository: git::OpenRepository, repo_config: RepoConfig, - addr: Addr, - message_token: gitforge::MessageToken, - ) { - match branch::validate_positions(self, &repository, repo_config).await { - Ok(branch::ValidatedPositions { - main, - next, - dev, - dev_commit_history, - }) => { - addr.do_send(StartMonitoring::new(main, next, dev, dev_commit_history)); - } - Err(err) => { - warn!("{:?}", err); - tokio::time::sleep(Duration::from_secs(10)).await; - addr.do_send(ValidateRepo::new(message_token)); - } - } + ) -> validation::Result { + branch::validate_positions(self, &repository, repo_config).await } fn branch_reset( @@ -85,7 +62,7 @@ impl super::ForgeLike for ForgeJoEnv { repository.push(&self.repo_details, branch_name, to_commit, force) } - async fn commit_status(&self, commit: &git::Commit) -> gitforge::CommitStatus { + async fn commit_status(&self, commit: &git::Commit) -> CommitStatus { let repo_details = &self.repo_details; let hostname = &repo_details.forge.hostname(); let repo_path = &repo_details.repo_path; @@ -110,21 +87,21 @@ impl super::ForgeLike for ForgeJoEnv { Ok(response) => { match response.response_body() { Some(status) => match status.state { - CommitStatusState::Success => gitforge::CommitStatus::Pass, - CommitStatusState::Pending => gitforge::CommitStatus::Pending, - CommitStatusState::Failure => gitforge::CommitStatus::Fail, - CommitStatusState::Error => gitforge::CommitStatus::Fail, - CommitStatusState::Blank => gitforge::CommitStatus::Pending, + CommitStatusState::Success => CommitStatus::Pass, + CommitStatusState::Pending => CommitStatus::Pending, + CommitStatusState::Failure => CommitStatus::Fail, + CommitStatusState::Error => CommitStatus::Fail, + CommitStatusState::Blank => CommitStatus::Pending, }, None => { warn!("No status found for commit"); - gitforge::CommitStatus::Pending // assume issue is transient and allow retry + CommitStatus::Pending // assume issue is transient and allow retry } } } Err(e) => { error!(?e, "Failed to get commit status"); - gitforge::CommitStatus::Pending // assume issue is transient and allow retry + CommitStatus::Pending // assume issue is transient and allow retry } } } diff --git a/crates/server/src/gitforge/github.rs b/crates/gitforge/src/github.rs similarity index 100% rename from crates/server/src/gitforge/github.rs rename to crates/gitforge/src/github.rs diff --git a/crates/server/src/gitforge/mod.rs b/crates/gitforge/src/lib.rs similarity index 76% rename from crates/server/src/gitforge/mod.rs rename to crates/gitforge/src/lib.rs index 3786e3f..343fa88 100644 --- a/crates/server/src/gitforge/mod.rs +++ b/crates/gitforge/src/lib.rs @@ -39,9 +39,7 @@ pub trait ForgeLike { &self, repository: OpenRepository, repo_config: RepoConfig, - addr: actix::prelude::Addr, - message_token: MessageToken, - ); + ) -> validation::Result; /// Moves a branch to a new commit. fn branch_reset( @@ -98,5 +96,33 @@ impl std::ops::Deref for Forge { } } +pub mod validation { + use git_next_config::BranchName; + use git_next_git as git; + + pub type Result = core::result::Result; + + pub struct Positions { + pub main: git::Commit, + pub next: git::Commit, + pub dev: git::Commit, + pub dev_commit_history: Vec, + } + + #[derive(Debug, derive_more::Display)] + pub enum Error { + Network(Box), + #[display("Failed to Reset Branch {branch} to {commit}")] + FailedToResetBranch { + branch: BranchName, + commit: git::Commit, + }, + BranchReset(BranchName), + BranchHasNoCommits(BranchName), + DevBranchNotBasedOn(BranchName), + } + impl std::error::Error for Error {} +} + #[cfg(test)] pub mod tests; diff --git a/crates/server/src/gitforge/mock_forge.rs b/crates/gitforge/src/mock_forge.rs similarity index 78% rename from crates/server/src/gitforge/mock_forge.rs rename to crates/gitforge/src/mock_forge.rs index d9f6668..31455d8 100644 --- a/crates/server/src/gitforge/mock_forge.rs +++ b/crates/gitforge/src/mock_forge.rs @@ -2,7 +2,7 @@ use git::OpenRepository; use git_next_config::{BranchName, GitDir, RepoConfig}; use git_next_git::{self as git, GitRef}; -use crate::{actors::repo::RepoActor, gitforge}; +use crate::{validation, CommitStatus, ForgeBranchError, ForgeFileError}; struct MockForge; #[derive(Clone, Debug)] @@ -18,7 +18,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, ForgeBranchError> { todo!() } @@ -26,7 +26,7 @@ impl super::ForgeLike for MockForgeEnv { &self, _branch: &BranchName, _file_path: &str, - ) -> Result { + ) -> Result { todo!() } @@ -34,9 +34,7 @@ impl super::ForgeLike for MockForgeEnv { &self, _repository: OpenRepository, _repo_config: RepoConfig, - _addr: actix::prelude::Addr, - _message_token: gitforge::MessageToken, - ) { + ) -> validation::Result { todo!() } @@ -50,7 +48,7 @@ impl super::ForgeLike for MockForgeEnv { todo!() } - async fn commit_status(&self, _commit: &git::Commit) -> gitforge::CommitStatus { + async fn commit_status(&self, _commit: &git::Commit) -> CommitStatus { todo!() } diff --git a/crates/server/src/gitforge/tests/data-forgejo-branches-get.json b/crates/gitforge/src/tests/data-forgejo-branches-get.json similarity index 100% rename from crates/server/src/gitforge/tests/data-forgejo-branches-get.json rename to crates/gitforge/src/tests/data-forgejo-branches-get.json diff --git a/crates/server/src/gitforge/tests/forgejo.rs b/crates/gitforge/src/tests/forgejo.rs similarity index 68% rename from crates/server/src/gitforge/tests/forgejo.rs rename to crates/gitforge/src/tests/forgejo.rs index 3edf231..58fc720 100644 --- a/crates/server/src/gitforge/tests/forgejo.rs +++ b/crates/gitforge/src/tests/forgejo.rs @@ -1,9 +1,10 @@ +// use assert2::let_assert; -use git_next_config::{ForgeType, RepoConfigSource}; +use git_next_config::{self as config, ForgeType, RepoConfigSource}; use kxio::network::{MockNetwork, StatusCode}; -use git_next_git::Generation; +use git_next_git::{self as git, Generation}; use super::*; @@ -14,11 +15,11 @@ fn test_name() { }; let net = Network::new_mock(); let (repo, _reality) = git::repository::mock(); - let repo_details = common::repo_details( + let repo_details = git::common::repo_details( 1, Generation::new(), - common::forge_details(1, ForgeType::MockForge), - Some(common::repo_config(1, RepoConfigSource::Repo)), + config::common::forge_details(1, ForgeType::MockForge), + Some(config::common::repo_config(1, RepoConfigSource::Repo)), GitDir::new(fs.base()), ); let forge = Forge::new_forgejo(repo_details, net, repo); @@ -31,9 +32,9 @@ async fn test_branches_get() { panic!("fs") }; let mut net = MockNetwork::new(); - let hostname = common::hostname(1); - let path = common::repo_path(1); - let api_token = common::api_token(1); + let hostname = config::common::hostname(1); + let path = config::common::repo_path(1); + let api_token = config::common::api_token(1); use secrecy::ExposeSecret; let token = api_token.expose_secret(); let url = format!("https://{hostname}/api/v1/repos/{path}/branches?token={token}"); @@ -42,11 +43,11 @@ async fn test_branches_get() { let net = Network::from(net); let (repo, _reality) = git::repository::mock(); - let repo_details = common::repo_details( + let repo_details = git::common::repo_details( 1, Generation::new(), - common::forge_details(1, ForgeType::MockForge), - Some(common::repo_config(1, RepoConfigSource::Repo)), + config::common::forge_details(1, ForgeType::MockForge), + Some(config::common::repo_config(1, RepoConfigSource::Repo)), GitDir::new(fs.base()), ); diff --git a/crates/server/src/gitforge/tests/github.rs b/crates/gitforge/src/tests/github.rs similarity index 100% rename from crates/server/src/gitforge/tests/github.rs rename to crates/gitforge/src/tests/github.rs diff --git a/crates/server/src/gitforge/tests/mod.rs b/crates/gitforge/src/tests/mod.rs similarity index 92% rename from crates/server/src/gitforge/tests/mod.rs rename to crates/gitforge/src/tests/mod.rs index eb07eca..52985a7 100644 --- a/crates/server/src/gitforge/tests/mod.rs +++ b/crates/gitforge/src/tests/mod.rs @@ -1,7 +1,5 @@ use super::*; -pub mod common; - #[cfg(feature = "forgejo")] mod forgejo; diff --git a/crates/server/src/gitforge/types.rs b/crates/gitforge/src/types.rs similarity index 100% rename from crates/server/src/gitforge/types.rs rename to crates/gitforge/src/types.rs diff --git a/crates/repo-actor/Cargo.toml b/crates/repo-actor/Cargo.toml new file mode 100644 index 0000000..304dcbf --- /dev/null +++ b/crates/repo-actor/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "git-next-repo-actor" +version = { workspace = true } +edition = { workspace = true } + +[features] +default = ["forgejo"] +forgejo = [] +github = [] + +[dependencies] +git-next-config = { workspace = true } +git-next-git = { workspace = true } +git-next-gitforge = { workspace = true } + +# logging +console-subscriber = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +# base64 decoding +base64 = { workspace = true } + +# git +async-trait = { workspace = true } + +# fs/network +kxio = { 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 } + +# boilerplate +derive_more = { workspace = true } + +# file watcher +inotify = { workspace = true } + +# Actors +actix = { workspace = true } +actix-rt = { workspace = true } +tokio = { workspace = true } + +[dev-dependencies] +# Testing +assert2 = { workspace = true } + +[lints.clippy] +nursery = { level = "warn", priority = -1 } +# pedantic = "warn" +unwrap_used = "warn" +expect_used = "warn" diff --git a/crates/server/src/actors/repo/branch.rs b/crates/repo-actor/src/branch.rs similarity index 82% rename from crates/server/src/actors/repo/branch.rs rename to crates/repo-actor/src/branch.rs index b7e01d8..eaf561f 100644 --- a/crates/server/src/actors/repo/branch.rs +++ b/crates/repo-actor/src/branch.rs @@ -2,14 +2,11 @@ use std::time::Duration; use actix::prelude::*; -use git_next_config::{RepoConfig, RepoConfigSource}; +use git_next_config::RepoConfig; use git_next_git as git; use tracing::{info, warn}; -use crate::{ - actors::repo::{LoadConfigFromRepo, RepoActor, ValidateRepo}, - gitforge, -}; +use crate::{gitforge, ValidateRepo}; // advance next to the next commit towards the head of the dev branch #[tracing::instrument(fields(next), skip_all)] @@ -80,23 +77,17 @@ pub fn find_next_commit_on_dev( #[tracing::instrument(fields(next), skip_all)] pub async fn advance_main( next: git::Commit, - repo_config: RepoConfig, - forge: gitforge::Forge, - repository: git::OpenRepository, - addr: Addr, - message_token: gitforge::MessageToken, + repo_config: &RepoConfig, + forge: &gitforge::Forge, + repository: &git::OpenRepository, ) { info!("Advancing main to next"); if let Err(err) = forge.branch_reset( - &repository, + repository, repo_config.branches().main(), next.into(), git::push::Force::No, ) { warn!(?err, "Failed") }; - match repo_config.source() { - RepoConfigSource::Repo => addr.do_send(LoadConfigFromRepo), - RepoConfigSource::Server => addr.do_send(ValidateRepo { message_token }), - } } diff --git a/crates/server/src/actors/repo/config.rs b/crates/repo-actor/src/config.rs similarity index 85% rename from crates/server/src/actors/repo/config.rs rename to crates/repo-actor/src/config.rs index 10dd68d..5017a50 100644 --- a/crates/server/src/actors/repo/config.rs +++ b/crates/repo-actor/src/config.rs @@ -2,7 +2,7 @@ use actix::prelude::*; use git_next_git::RepoDetails; use tracing::{error, info}; -use crate::{config, gitforge}; +use crate::{gitforge, load}; use super::{LoadedConfig, RepoActor}; @@ -10,7 +10,7 @@ use super::{LoadedConfig, RepoActor}; #[tracing::instrument(skip_all, fields(branch = %repo_details.branch))] 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 { + let repo_config = match load::load(&repo_details, &forge).await { Ok(repo_config) => repo_config, Err(err) => { error!(?err, "Failed to load config"); diff --git a/crates/server/src/actors/repo/mod.rs b/crates/repo-actor/src/lib.rs similarity index 87% rename from crates/server/src/actors/repo/mod.rs rename to crates/repo-actor/src/lib.rs index 68ba532..e8210f4 100644 --- a/crates/server/src/actors/repo/mod.rs +++ b/crates/repo-actor/src/lib.rs @@ -1,19 +1,25 @@ mod branch; -mod config; +pub mod config; +mod load; pub mod status; pub mod webhook; #[cfg(test)] mod tests; +use std::time::Duration; + use actix::prelude::*; use git::OpenRepository; -use git_next_config::{ForgeType, RepoConfig}; +use git_next_config::{server::Webhook, ForgeType, RepoConfig, RepoConfigSource}; use git_next_git::{self as git, Generation, RepoDetails}; +use git_next_gitforge::{self as gitforge, validation}; use kxio::network::Network; use tracing::{debug, info, warn, Instrument}; -use crate::{actors::repo::webhook::WebhookAuth, config::Webhook, gitforge}; +// use crate::{actors::repo::webhook::WebhookAuth, config::Webhook, gitforge}; + +use crate::webhook::WebhookAuth; use self::webhook::WebhookId; @@ -181,9 +187,24 @@ impl Handler for RepoActor { let addr = ctx.address(); let message_token = self.message_token; async move { - forge - .branches_validate_positions(repository, repo_config, addr, message_token) + match forge + .branches_validate_positions(repository, repo_config) .await + { + Ok(validation::Positions { + main, + next, + dev, + dev_commit_history, + }) => { + addr.do_send(StartMonitoring::new(main, next, dev, dev_commit_history)); + } + Err(err) => { + warn!("{:?}", err); + tokio::time::sleep(Duration::from_secs(10)).await; + addr.do_send(ValidateRepo::new(message_token)); + } + } } .in_current_span() .into_actor(self) @@ -271,14 +292,14 @@ impl Handler for RepoActor { }; let forge = self.forge.clone(); let addr = ctx.address(); - branch::advance_main( - msg.0, - repo_config, - forge, - repository, - addr, - self.message_token, - ) + let message_token = self.message_token; + async move { + branch::advance_main(msg.0, &repo_config, &forge, &repository).await; + match repo_config.source() { + RepoConfigSource::Repo => addr.do_send(LoadConfigFromRepo), + RepoConfigSource::Server => addr.do_send(ValidateRepo { message_token }), + } + } .in_current_span() .into_actor(self) .wait(ctx); diff --git a/crates/server/src/config/load.rs b/crates/repo-actor/src/load.rs similarity index 93% rename from crates/server/src/config/load.rs rename to crates/repo-actor/src/load.rs index 68d0ea5..06ad479 100644 --- a/crates/server/src/config/load.rs +++ b/crates/repo-actor/src/load.rs @@ -1,4 +1,4 @@ -use git_next_config::{BranchName, RepoConfig}; +use git_next_config::{self as config, BranchName, RepoConfig}; use git_next_git::RepoDetails; use tracing::error; @@ -16,7 +16,7 @@ pub async fn load(details: &RepoDetails, forge: &gitforge::Forge) -> Result, - forge: gitforge::Forge, - message_token: gitforge::MessageToken, + forge: Forge, + message_token: MessageToken, ) { // get the status - pass, fail, pending (all others map to fail, e.g. error) let status = forge.commit_status(&next).await; info!(?status, "Checking next branch"); match status { - gitforge::CommitStatus::Pass => { + CommitStatus::Pass => { addr.do_send(AdvanceMainTo(next)); } - gitforge::CommitStatus::Pending => { + CommitStatus::Pending => { tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; addr.do_send(ValidateRepo { message_token }); } - gitforge::CommitStatus::Fail => { + CommitStatus::Fail => { warn!("Checks have failed"); } } diff --git a/crates/server/src/actors/repo/tests.rs b/crates/repo-actor/src/tests.rs similarity index 100% rename from crates/server/src/actors/repo/tests.rs rename to crates/repo-actor/src/tests.rs diff --git a/crates/server/src/actors/repo/webhook.rs b/crates/repo-actor/src/webhook.rs similarity index 92% rename from crates/server/src/actors/repo/webhook.rs rename to crates/repo-actor/src/webhook.rs index 659ad06..431a2a8 100644 --- a/crates/server/src/actors/repo/webhook.rs +++ b/crates/repo-actor/src/webhook.rs @@ -1,5 +1,8 @@ use actix::prelude::*; -use git_next_config::{BranchName, RepoBranches}; +use git_next_config::{ + server::{Webhook, WebhookUrl}, + BranchName, RepoAlias, RepoBranches, +}; use git_next_git::{self as git, RepoDetails}; use kxio::network::{self, json}; use tracing::{info, warn}; @@ -7,13 +10,7 @@ use ulid::DecodeError; use std::{collections::HashMap, str::FromStr}; -use crate::{ - actors::{ - repo::{RepoActor, ValidateRepo, WebhookRegistered}, - webhook::WebhookMessage, - }, - config::{Webhook, WebhookUrl}, -}; +use crate::{RepoActor, ValidateRepo, WebhookRegistered}; #[derive( Clone, Debug, PartialEq, Eq, derive_more::Constructor, derive_more::Deref, derive_more::Display, @@ -23,7 +20,7 @@ pub struct WebhookId(String); #[derive(Clone, Debug, PartialEq, Eq, derive_more::Deref, derive_more::Display)] pub struct WebhookAuth(ulid::Ulid); impl WebhookAuth { - pub fn from_str(authorisation: &str) -> Result { + pub fn new(authorisation: &str) -> Result { let id = ulid::Ulid::from_str(authorisation)?; info!("Parse auth token: {}", id); Ok(Self(id)) @@ -307,3 +304,31 @@ pub enum Branch { struct HeadCommit { message: String, } + +#[derive(Message, Debug, Clone, derive_more::Constructor)] +#[rtype(result = "()")] +pub struct WebhookMessage { + // forge // TODO: (#58) differentiate between multiple forges + repo_alias: RepoAlias, + authorisation: WebhookAuth, + body: Body, +} +impl WebhookMessage { + pub const fn repo_alias(&self) -> &RepoAlias { + &self.repo_alias + } + pub const fn body(&self) -> &Body { + &self.body + } + pub const fn authorisation(&self) -> &WebhookAuth { + &self.authorisation + } +} + +#[derive(Clone, Debug, derive_more::Constructor)] +pub struct Body(String); +impl Body { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 56187d4..a6baf18 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -11,6 +11,8 @@ github = [] [dependencies] git-next-config = { workspace = true } git-next-git = { workspace = true } +git-next-gitforge = { workspace = true } +git-next-repo-actor = { workspace = true } # logging console-subscriber = { workspace = true } diff --git a/crates/server/src/actors/mod.rs b/crates/server/src/actors/mod.rs index 92309df..2fd2c6f 100644 --- a/crates/server/src/actors/mod.rs +++ b/crates/server/src/actors/mod.rs @@ -1,4 +1,3 @@ pub mod file_watcher; -pub mod repo; pub mod server; pub mod webhook; diff --git a/crates/server/src/actors/server.rs b/crates/server/src/actors/server.rs index eb6f926..9809ab0 100644 --- a/crates/server/src/actors/server.rs +++ b/crates/server/src/actors/server.rs @@ -1,19 +1,20 @@ +// use std::path::PathBuf; use actix::prelude::*; -use git_next_config::{ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig}; +use config::server::{ServerConfig, ServerStorage, Webhook}; +use git_next_config::{ + self as config, ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig, +}; use git_next_git::{Generation, RepoDetails, Repository}; +use git_next_repo_actor::{CloneRepo, RepoActor}; use kxio::{fs::FileSystem, network::Network}; use tracing::{error, info, warn}; -use crate::{ - actors::{ - file_watcher::FileUpdated, - repo::{CloneRepo, RepoActor}, - webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter}, - }, - config::{ServerConfig, ServerStorage, Webhook}, +use crate::actors::{ + file_watcher::FileUpdated, + webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter}, }; #[derive(Debug, derive_more::Display, derive_more::From)] @@ -26,7 +27,7 @@ pub enum Error { path: PathBuf, }, - Config(crate::config::Error), + Config(config::server::Error), Io(std::io::Error), } diff --git a/crates/server/src/actors/webhook/message.rs b/crates/server/src/actors/webhook/message.rs deleted file mode 100644 index 6c90f6a..0000000 --- a/crates/server/src/actors/webhook/message.rs +++ /dev/null @@ -1,33 +0,0 @@ -// -use actix::prelude::*; -use git_next_config::RepoAlias; - -use crate::actors::repo::webhook::WebhookAuth; - -#[derive(Message, Debug, Clone, derive_more::Constructor)] -#[rtype(result = "()")] -pub struct WebhookMessage { - // forge // TODO: (#58) differentiate between multiple forges - repo_alias: RepoAlias, - authorisation: WebhookAuth, - body: Body, -} -impl WebhookMessage { - pub const fn repo_alias(&self) -> &RepoAlias { - &self.repo_alias - } - pub const fn body(&self) -> &Body { - &self.body - } - pub const fn authorisation(&self) -> &WebhookAuth { - &self.authorisation - } -} - -#[derive(Clone, Debug, derive_more::Constructor)] -pub struct Body(String); -impl Body { - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} diff --git a/crates/server/src/actors/webhook/mod.rs b/crates/server/src/actors/webhook/mod.rs index c9d42d8..885fc75 100644 --- a/crates/server/src/actors/webhook/mod.rs +++ b/crates/server/src/actors/webhook/mod.rs @@ -1,6 +1,5 @@ // crate::server::actors::webhook -mod message; mod router; mod server; @@ -8,7 +7,7 @@ use std::net::SocketAddr; use actix::prelude::*; -pub use message::WebhookMessage; +use git_next_repo_actor::webhook::WebhookMessage; pub use router::AddWebhookRecipient; pub use router::WebhookRouter; use tracing::Instrument; diff --git a/crates/server/src/actors/webhook/router.rs b/crates/server/src/actors/webhook/router.rs index e5a9210..84efbb6 100644 --- a/crates/server/src/actors/webhook/router.rs +++ b/crates/server/src/actors/webhook/router.rs @@ -1,11 +1,11 @@ +// use std::collections::HashMap; use actix::prelude::*; use git_next_config::RepoAlias; +use git_next_repo_actor::webhook::WebhookMessage; use tracing::{debug, info}; -use crate::actors::webhook::message::WebhookMessage; - pub struct WebhookRouter { span: tracing::Span, repos: HashMap>, diff --git a/crates/server/src/actors/webhook/server.rs b/crates/server/src/actors/webhook/server.rs index 4463ae5..927581b 100644 --- a/crates/server/src/actors/webhook/server.rs +++ b/crates/server/src/actors/webhook/server.rs @@ -1,19 +1,14 @@ +// use std::net::SocketAddr; use actix::prelude::*; use git_next_config::RepoAlias; +use git_next_repo_actor::webhook::{self, WebhookAuth, WebhookMessage}; use tracing::{info, warn}; use warp::reject::Rejection; -use crate::actors::{repo::webhook::WebhookAuth, webhook::message::WebhookMessage}; - -use super::message; - -pub async fn start( - socket_addr: SocketAddr, - address: actix::prelude::Recipient, -) { +pub async fn start(socket_addr: SocketAddr, address: actix::prelude::Recipient) { // start webhook server use warp::Filter; // Define the Warp route to handle incoming HTTP requests @@ -32,7 +27,7 @@ pub async fn start( info!("POST received"); let repo_alias = RepoAlias::new(path); let bytes = body.to_vec(); - let body = message::Body::new(String::from_utf8_lossy(&bytes).to_string()); + let body = webhook::Body::new(String::from_utf8_lossy(&bytes).to_string()); headers.get("Authorization").map_or_else( || { warn!("No Authorization header"); @@ -70,7 +65,7 @@ pub async fn start( } fn parse_auth(authorization_header: &warp::http::HeaderValue) -> Result { - WebhookAuth::from_str( + WebhookAuth::new( authorization_header .to_str() .map_err(|e| { diff --git a/crates/server/src/config/mod.rs b/crates/server/src/config/mod.rs index 83ef965..87c2771 100644 --- a/crates/server/src/config/mod.rs +++ b/crates/server/src/config/mod.rs @@ -1,107 +1,2 @@ -use actix::prelude::*; - -pub mod load; - -use std::{ - collections::HashMap, - net::SocketAddr, - path::{Path, PathBuf}, - str::FromStr, -}; - -use git_next_config::{ForgeConfig, ForgeName}; -use kxio::fs::FileSystem; -use tracing::info; - -#[derive(Debug, derive_more::From, derive_more::Display)] -pub enum Error { - Io(std::io::Error), - KxIoFs(kxio::fs::Error), - TomlDe(toml::de::Error), - AddressParse(std::net::AddrParseError), -} -impl std::error::Error for Error {} - -type Result = core::result::Result; - -/// Mapped from the `git-next-server.toml` file -#[derive(Debug, PartialEq, Eq, serde::Deserialize, Message)] -#[rtype(result = "()")] -pub struct ServerConfig { - http: Http, - webhook: Webhook, - storage: ServerStorage, - forge: HashMap, -} -impl ServerConfig { - #[tracing::instrument(skip_all)] - pub(crate) fn load(fs: &FileSystem) -> Result { - let file = fs.base().join("git-next-server.toml"); - info!(?file, ""); - let str = fs.file_read_to_string(&file)?; - toml::from_str(&str).map_err(Into::into) - } - - pub(crate) fn forges(&self) -> impl Iterator { - self.forge - .iter() - .map(|(name, forge)| (ForgeName::new(name.clone()), forge)) - } - - pub const fn storage(&self) -> &ServerStorage { - &self.storage - } - - pub const fn webhook(&self) -> &Webhook { - &self.webhook - } - - pub fn http(&self) -> Result { - self.http.socket_addr() - } -} - -/// Defines the port the server will listen to for incoming webhooks messages -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -pub struct Http { - addr: String, - port: u16, -} -impl Http { - fn socket_addr(&self) -> Result { - Ok(SocketAddr::from_str(&format!( - "{}:{}", - self.addr, self.port - ))?) - } -} - -/// Defines the Webhook Forges should send updates to -/// Must be an address that is accessible from the remote forge -#[derive(Clone, Debug, PartialEq, Eq, serde::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, serde::Deserialize, derive_more::AsRef)] -pub struct WebhookUrl(String); - -/// The directory to store server data, such as cloned repos -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -pub struct ServerStorage { - path: PathBuf, -} -impl ServerStorage { - pub fn path(&self) -> &Path { - self.path.as_path() - } -} - #[cfg(test)] mod tests; diff --git a/crates/server/src/config/tests.rs b/crates/server/src/config/tests.rs index c19a48e..5d82ea9 100644 --- a/crates/server/src/config/tests.rs +++ b/crates/server/src/config/tests.rs @@ -1,132 +1,14 @@ -use std::collections::BTreeMap; - +// use assert2::let_assert; use git::repository::Direction; use git_next_config::{ - ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, - ServerRepoConfig, + self as config, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, + RepoPath, }; use git_next_git::{self as git, Generation, GitRemote}; -use kxio::fs; - -use crate::gitforge::tests::common; - -use super::*; type Result = core::result::Result>; -#[test] -fn load_should_parse_server_config() -> Result<()> { - let fs = fs::temp()?; - fs.file_write( - &fs.base().join("git-next-server.toml"), - r#" - [http] - addr = "0.0.0.0" - port = 8080 - - [webhook] - url = "http://localhost:9909/webhook" - - [storage] - path = "/opt/git-next/data" - - [forge.default] - forge_type = "MockForge" - hostname = "git.example.net" - user = "Bob" - token = "API-Token" - - [forge.default.repos] - hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/user/hello.git" } - world = { repo = "user/world", branch = "master", main = "main", next = "next", dev = "dev" } - - [forge.default.repos.sam] - repo = "user/sam" - branch = "main" - main = "master" - next = "upcoming" - dev = "sam-dev" - "#, - ) - ?; - let_assert!(Ok(config) = ServerConfig::load(&fs)); - let expected = ServerConfig { - http: Http { - addr: "0.0.0.0".to_string(), - port: 8080, - }, - webhook: Webhook { - url: "http://localhost:9909/webhook".to_string(), - }, - storage: ServerStorage { - path: "/opt/git-next/data".into(), - }, - forge: HashMap::from([( - "default".to_string(), - ForgeConfig::new( - ForgeType::MockForge, - "git.example.net".to_string(), - "Bob".to_string(), - "API-Token".to_string(), - BTreeMap::from([ - ( - "hello".to_string(), - ServerRepoConfig::new( - "user/hello".to_string(), - "main".to_string(), - Some("/opt/git/user/hello.git".into()), - None, - None, - None, - ), - ), - ( - "world".to_string(), - ServerRepoConfig::new( - "user/world".to_string(), - "master".to_string(), - None, - Some("main".to_string()), - Some("next".to_string()), - Some("dev".to_string()), - ), - ), - ( - "sam".to_string(), - ServerRepoConfig::new( - "user/sam".to_string(), - "main".to_string(), - None, - Some("master".to_string()), - Some("upcoming".to_string()), - Some("sam-dev".to_string()), - ), - ), - ]), - ), - )]), - }; - assert_eq!(config, expected, "ServerConfig"); - - if let Some(forge) = config.forge.get("world") { - if let Some(repo) = forge.get_repo("sam") { - let repo_config = repo.repo_config(); - let expected = Some(RepoConfig::new( - RepoBranches::new( - "master".to_string(), - "upcoming".to_string(), - "sam-dev".to_string(), - ), - RepoConfigSource::Server, - )); - assert_eq!(repo_config, expected, "RepoConfig"); - } - } - - Ok(()) -} - #[test] fn test_repo_config_load() -> Result<()> { let toml = r#"[branches] @@ -166,10 +48,10 @@ fn gitdir_should_display_as_pathbuf() { fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { 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( + let mut repo_details = git::common::repo_details( 1, Generation::new(), - common::forge_details(1, ForgeType::MockForge), + config::common::forge_details(1, ForgeType::MockForge), None, GitDir::new(root), // Server GitDir - should be ignored ); @@ -197,10 +79,10 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> { 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( + let mut repo_details = git::common::repo_details( 1, Generation::new(), - common::forge_details(1, ForgeType::MockForge), + config::common::forge_details(1, ForgeType::MockForge), None, GitDir::new(root), // Server GitDir - should be ignored ); @@ -219,10 +101,10 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> { fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> { 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( + let mut repo_details = git::common::repo_details( 1, Generation::new(), - common::forge_details(1, ForgeType::MockForge), + config::common::forge_details(1, ForgeType::MockForge), None, GitDir::new(root), // Server GitDir - should be ignored ); diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 8a71abb..e264869 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,7 +1,6 @@ mod actors; mod config; -pub mod gitforge; - +// use actix::prelude::*; use git_next_git::Repository;