diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 2f5cbc7b..2c5ef429 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,6 +15,9 @@ categories = { workspace = true } git-next-core = { workspace = true } git-next-file-watcher-actor = { workspace = true } git-next-server-actor = { workspace = true } +git-next-forge = { workspace = true } +git-next-repo-actor = { workspace = true } +git-next-webhook-actor = { workspace = true } # CLI parsing clap = { workspace = true } @@ -31,10 +34,21 @@ tracing-subscriber = { workspace = true } actix = { workspace = true } actix-rt = { workspace = true } +# boilerplate +derive_more = { workspace = true } +derive-with = { workspace = true } + +# Webhooks +serde = { workspace = true } +serde_json = { workspace = true } +ulid = { workspace = true } +time = { workspace = true } +secrecy = { workspace = true } +standardwebhooks = { workspace = true } + [dev-dependencies] # Testing assert2 = { workspace = true } -secrecy = { workspace = true } test-log = { workspace = true } [lints.clippy] diff --git a/crates/cli/README.md b/crates/cli/README.md index c27d6463..f8b52f90 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -520,7 +520,9 @@ The following diagram shows the dependency between the crates that make up `git- stateDiagram-v2 cli --> core - cli --> server_actor + cli --> forge + cli --> repo_actor + cli --> webhook_actor cli --> file_watcher_actor forge --> core @@ -534,12 +536,6 @@ stateDiagram-v2 repo_actor --> core repo_actor --> forge - server_actor --> core - server_actor --> forge - server_actor --> repo_actor - server_actor --> file_watcher_actor - server_actor --> webhook_actor - webhook_actor --> core webhook_actor --> repo_actor ``` diff --git a/crates/server-actor/src/handlers/file_updated.rs b/crates/cli/src/server/actor/handlers/file_updated.rs similarity index 89% rename from crates/server-actor/src/handlers/file_updated.rs rename to crates/cli/src/server/actor/handlers/file_updated.rs index 10695525..64fef781 100644 --- a/crates/server-actor/src/handlers/file_updated.rs +++ b/crates/cli/src/server/actor/handlers/file_updated.rs @@ -4,7 +4,7 @@ use actix::prelude::*; use git_next_core::server::ServerConfig; use git_next_file_watcher_actor::FileUpdated; -use crate::{messages::ReceiveServerConfig, ServerActor}; +use crate::server::actor::{messages::ReceiveServerConfig, ServerActor}; impl Handler for ServerActor { type Result = (); diff --git a/crates/server-actor/src/handlers/mod.rs b/crates/cli/src/server/actor/handlers/mod.rs similarity index 100% rename from crates/server-actor/src/handlers/mod.rs rename to crates/cli/src/server/actor/handlers/mod.rs diff --git a/crates/server-actor/src/handlers/notify_user.rs b/crates/cli/src/server/actor/handlers/notify_user.rs similarity index 98% rename from crates/server-actor/src/handlers/notify_user.rs rename to crates/cli/src/server/actor/handlers/notify_user.rs index 641c4ae9..ca44408f 100644 --- a/crates/server-actor/src/handlers/notify_user.rs +++ b/crates/cli/src/server/actor/handlers/notify_user.rs @@ -5,7 +5,7 @@ use secrecy::ExposeSecret; use standardwebhooks::Webhook; use tracing::Instrument; -use crate::ServerActor; +use crate::server::actor::ServerActor; use git_next_core::server::{self, Notification, NotificationType}; use git_next_repo_actor::messages::NotifyUser; diff --git a/crates/server-actor/src/handlers/receive_server_config.rs b/crates/cli/src/server/actor/handlers/receive_server_config.rs similarity index 97% rename from crates/server-actor/src/handlers/receive_server_config.rs rename to crates/cli/src/server/actor/handlers/receive_server_config.rs index 5465d8c0..889f5ed2 100644 --- a/crates/server-actor/src/handlers/receive_server_config.rs +++ b/crates/cli/src/server/actor/handlers/receive_server_config.rs @@ -1,6 +1,6 @@ use actix::prelude::*; -use crate::{ +use crate::server::actor::{ messages::{ReceiveServerConfig, ReceiveValidServerConfig, ValidServerConfig}, ServerActor, }; diff --git a/crates/server-actor/src/handlers/receive_valid_server_config.rs b/crates/cli/src/server/actor/handlers/receive_valid_server_config.rs similarity index 98% rename from crates/server-actor/src/handlers/receive_valid_server_config.rs rename to crates/cli/src/server/actor/handlers/receive_valid_server_config.rs index f2fd8b4c..17fbb2d4 100644 --- a/crates/server-actor/src/handlers/receive_valid_server_config.rs +++ b/crates/cli/src/server/actor/handlers/receive_valid_server_config.rs @@ -1,7 +1,7 @@ use actix::prelude::*; use git_next_webhook_actor::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter}; -use crate::{ +use crate::server::actor::{ messages::{ReceiveValidServerConfig, ValidServerConfig}, ServerActor, }; diff --git a/crates/server-actor/src/handlers/shutdown.rs b/crates/cli/src/server/actor/handlers/shutdown.rs similarity index 93% rename from crates/server-actor/src/handlers/shutdown.rs rename to crates/cli/src/server/actor/handlers/shutdown.rs index c4211496..92b05f14 100644 --- a/crates/server-actor/src/handlers/shutdown.rs +++ b/crates/cli/src/server/actor/handlers/shutdown.rs @@ -2,7 +2,7 @@ use actix::prelude::*; use git_next_webhook_actor::ShutdownWebhook; -use crate::{messages::Shutdown, ServerActor}; +use crate::server::actor::{messages::Shutdown, ServerActor}; impl Handler for ServerActor { type Result = (); diff --git a/crates/server-actor/src/messages.rs b/crates/cli/src/server/actor/messages.rs similarity index 100% rename from crates/server-actor/src/messages.rs rename to crates/cli/src/server/actor/messages.rs diff --git a/crates/cli/src/server/actor/mod.rs b/crates/cli/src/server/actor/mod.rs new file mode 100644 index 00000000..fbaca2c5 --- /dev/null +++ b/crates/cli/src/server/actor/mod.rs @@ -0,0 +1,244 @@ +// +use actix::prelude::*; + +#[cfg(test)] +mod tests; + +mod handlers; +pub mod messages; + +use git_next_core::{ + git::{repository::factory::RepositoryFactory, Generation, RepoDetails}, + server::{self, InboundWebhook, ServerConfig, ServerStorage}, + ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, +}; +use git_next_repo_actor::messages::NotifyUser; +use git_next_repo_actor::{messages::CloneRepo, RepoActor}; +use git_next_webhook_actor::WebhookActor; + +use kxio::{fs::FileSystem, network::Network}; + +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{Arc, RwLock}, +}; + +use tracing::{error, info}; + +use messages::ReceiveServerConfig; + +#[derive(Debug, derive_more::Display, derive_more::From)] +pub enum Error { + #[display("Failed to create data directories")] + FailedToCreateDataDirectory(kxio::fs::Error), + + #[display("The forge data path is not a directory: {path:?}")] + ForgeDirIsNotDirectory { + path: PathBuf, + }, + + Config(server::Error), + + Io(std::io::Error), +} +type Result = core::result::Result; + +#[derive(derive_with::With)] +#[with(message_log)] +pub struct ServerActor { + server_config: Option, + generation: Generation, + webhook_actor_addr: Option>, + fs: FileSystem, + net: Network, + repository_factory: Box, + sleep_duration: std::time::Duration, + repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr>, + + // testing + message_log: Option>>>, +} +impl Actor for ServerActor { + type Context = Context; +} + +impl ServerActor { + pub fn new( + fs: FileSystem, + net: Network, + repo: Box, + sleep_duration: std::time::Duration, + ) -> Self { + let generation = Generation::default(); + Self { + server_config: None, + generation, + webhook_actor_addr: None, + fs, + net, + repository_factory: repo, + sleep_duration, + repo_actors: BTreeMap::new(), + message_log: None, + } + } + fn create_forge_data_directories( + &self, + server_config: &ServerConfig, + server_dir: &std::path::Path, + ) -> Result<()> { + for (forge_name, _forge_config) in server_config.forges() { + let forge_dir: PathBuf = (&forge_name).into(); + let path = server_dir.join(&forge_dir); + if self.fs.path_exists(&path)? { + if !self.fs.path_is_dir(&path)? { + return Err(Error::ForgeDirIsNotDirectory { path }); + } + } else { + info!(%forge_name, ?path, "creating storage"); + self.fs.dir_create_all(&path)?; + } + } + + Ok(()) + } + + fn create_forge_repos( + &self, + forge_config: &ForgeConfig, + forge_name: ForgeAlias, + server_storage: &ServerStorage, + webhook: &InboundWebhook, + notify_user_recipient: Recipient, + ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { + let span = + tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); + + let _guard = span.enter(); + info!("Creating Forge"); + let mut repos = vec![]; + let creator = self.create_actor(forge_name, forge_config.clone(), server_storage, webhook); + for (repo_alias, server_repo_config) in forge_config.repos() { + let forge_repo = creator(( + repo_alias, + server_repo_config, + notify_user_recipient.clone(), + )); + info!( + alias = %forge_repo.1, + "Created Repo" + ); + repos.push(forge_repo); + } + repos + } + + fn create_actor( + &self, + forge_name: ForgeAlias, + forge_config: ForgeConfig, + server_storage: &ServerStorage, + webhook: &InboundWebhook, + ) -> impl Fn( + (RepoAlias, &ServerRepoConfig, Recipient), + ) -> (ForgeAlias, RepoAlias, RepoActor) { + let server_storage = server_storage.clone(); + let webhook = webhook.clone(); + let net = self.net.clone(); + let repository_factory = self.repository_factory.duplicate(); + let generation = self.generation; + let sleep_duration = self.sleep_duration; + // let notify_user_recipient = server_addr.recipient(); + move |(repo_alias, server_repo_config, notify_user_recipient)| { + let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); + let _guard = span.enter(); + info!("Creating Repo"); + let gitdir = server_repo_config.gitdir().map_or_else( + || { + GitDir::new( + server_storage + .path() + .join(forge_name.to_string()) + .join(repo_alias.to_string()), + StoragePathType::Internal, + ) + }, + |gitdir| gitdir, + ); + // INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not + // have cloned the repo yet + let repo_details = RepoDetails::new( + generation, + &repo_alias, + server_repo_config, + &forge_name, + &forge_config, + gitdir, + ); + let forge = git_next_forge::Forge::create(repo_details.clone(), net.clone()); + info!("Starting Repo Actor"); + let actor = RepoActor::new( + repo_details, + forge, + webhook.clone(), + generation, + net.clone(), + repository_factory.duplicate(), + sleep_duration, + Some(notify_user_recipient), + ); + (forge_name.clone(), repo_alias, actor) + } + } + + fn start_actor( + &self, + actor: (ForgeAlias, RepoAlias, RepoActor), + ) -> (RepoAlias, Addr) { + let (forge_name, repo_alias, actor) = actor; + let span = tracing::info_span!("start_actor", forge = %forge_name, repo = %repo_alias); + let _guard = span.enter(); + let addr = actor.start(); + addr.do_send(CloneRepo); + info!("Started"); + (repo_alias, addr) + } + + fn server_storage(&self, server_config: &ReceiveServerConfig) -> Option { + let server_storage = server_config.storage().clone(); + let dir = server_storage.path(); + if !dir.exists() { + if let Err(err) = self.fs.dir_create(dir) { + error!(?err, ?dir, "Failed to create server storage"); + return None; + } + } + let Ok(canon) = dir.canonicalize() else { + error!(?dir, "Failed to confirm server storage"); + return None; + }; + if let Err(err) = self.create_forge_data_directories(server_config, &canon) { + error!(?err, "Failure creating forge storage"); + return None; + } + Some(server_storage) + } + + fn do_send(&mut self, msg: M, _ctx: &mut ::Context) + where + M: actix::Message + Send + 'static + std::fmt::Debug, + Self: actix::Handler, + ::Result: Send, + { + if let Some(message_log) = &self.message_log { + let log_message = format!("send: {:?}", msg); + if let Ok(mut log) = message_log.write() { + log.push(log_message); + } + } + #[cfg(not(test))] + _ctx.address().do_send(msg); + tracing::info!("sent"); + } +} diff --git a/crates/server-actor/src/tests/given.rs b/crates/cli/src/server/actor/tests/given.rs similarity index 100% rename from crates/server-actor/src/tests/given.rs rename to crates/cli/src/server/actor/tests/given.rs diff --git a/crates/server-actor/src/tests/mod.rs b/crates/cli/src/server/actor/tests/mod.rs similarity index 100% rename from crates/server-actor/src/tests/mod.rs rename to crates/cli/src/server/actor/tests/mod.rs diff --git a/crates/server-actor/src/tests/receive_server_config.rs b/crates/cli/src/server/actor/tests/receive_server_config.rs similarity index 91% rename from crates/server-actor/src/tests/receive_server_config.rs rename to crates/cli/src/server/actor/tests/receive_server_config.rs index f7cedc2e..4225e94e 100644 --- a/crates/server-actor/src/tests/receive_server_config.rs +++ b/crates/cli/src/server/actor/tests/receive_server_config.rs @@ -1,7 +1,7 @@ // use actix::prelude::*; -use crate::{tests::given, ReceiveServerConfig, ServerActor}; +use crate::server::actor::{tests::given, ReceiveServerConfig, ServerActor}; use git_next_core::{ git, server::{Http, InboundWebhook, Notification, ServerConfig, ServerStorage}, @@ -45,7 +45,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { server_storage, repos, ))); - tokio::time::sleep(std::time::Duration::from_millis(1)).await; + actix_rt::time::sleep(std::time::Duration::from_millis(1)).await; //then // INFO: assert that ReceiveValidServerConfig is NOT sent diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index 91953faf..9fbf0cec 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -1,12 +1,14 @@ // +mod actor; + #[cfg(test)] mod tests; use actix::prelude::*; +use actor::ServerActor; use git_next_core::git::RepositoryFactory; use git_next_file_watcher_actor::{FileUpdated, FileWatcher}; -use git_next_server_actor::ServerActor; use kxio::{fs::FileSystem, network::Network}; use tracing::{error, info, level_filters::LevelFilter}; @@ -61,7 +63,7 @@ pub fn start( info!("Server running - Press Ctrl-C to stop..."); let _ = actix_rt::signal::ctrl_c().await; info!("Ctrl-C received, shutting down..."); - server.do_send(git_next_server_actor::messages::Shutdown); + server.do_send(crate::server::actor::messages::Shutdown); actix_rt::time::sleep(std::time::Duration::from_millis(200)).await; System::current().stop(); }; diff --git a/crates/server-actor/Cargo.toml b/crates/server-actor/Cargo.toml index ee3d5aee..e7794022 100644 --- a/crates/server-actor/Cargo.toml +++ b/crates/server-actor/Cargo.toml @@ -7,32 +7,7 @@ repository = { workspace = true } description = "Server actor for git-next, the trunk-based development manager" [dependencies] -git-next-core = { workspace = true } -git-next-forge = { workspace = true } -git-next-repo-actor = { workspace = true } -git-next-file-watcher-actor = { workspace = true } -git-next-webhook-actor = { workspace = true } -# logging -tracing = { workspace = true } - -# fs/network -kxio = { workspace = true } - -# boilerplate -derive_more = { workspace = true } -derive-with = { workspace = true } - -# Actors -actix = { workspace = true } - -# Webhooks -serde = { workspace = true } -serde_json = { workspace = true } -ulid = { workspace = true } -time = { workspace = true } -secrecy = { workspace = true } -standardwebhooks = { workspace = true } [dev-dependencies] # Testing diff --git a/crates/server-actor/README.md b/crates/server-actor/README.md index ddc70e2a..b90af458 100644 --- a/crates/server-actor/README.md +++ b/crates/server-actor/README.md @@ -7,3 +7,5 @@ development workflows where each commit must pass CI before being included in the main branch. See [git-next](https://crates.io/crates/git-next) for more information. + +N.B. this crate has been merged into [git-next](https://crates.io/git-next). diff --git a/crates/server-actor/src/lib.rs b/crates/server-actor/src/lib.rs index fbaca2c5..749ded1b 100644 --- a/crates/server-actor/src/lib.rs +++ b/crates/server-actor/src/lib.rs @@ -1,244 +1 @@ -// -use actix::prelude::*; - -#[cfg(test)] -mod tests; - -mod handlers; -pub mod messages; - -use git_next_core::{ - git::{repository::factory::RepositoryFactory, Generation, RepoDetails}, - server::{self, InboundWebhook, ServerConfig, ServerStorage}, - ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, -}; -use git_next_repo_actor::messages::NotifyUser; -use git_next_repo_actor::{messages::CloneRepo, RepoActor}; -use git_next_webhook_actor::WebhookActor; - -use kxio::{fs::FileSystem, network::Network}; - -use std::{ - collections::BTreeMap, - path::PathBuf, - sync::{Arc, RwLock}, -}; - -use tracing::{error, info}; - -use messages::ReceiveServerConfig; - -#[derive(Debug, derive_more::Display, derive_more::From)] -pub enum Error { - #[display("Failed to create data directories")] - FailedToCreateDataDirectory(kxio::fs::Error), - - #[display("The forge data path is not a directory: {path:?}")] - ForgeDirIsNotDirectory { - path: PathBuf, - }, - - Config(server::Error), - - Io(std::io::Error), -} -type Result = core::result::Result; - -#[derive(derive_with::With)] -#[with(message_log)] -pub struct ServerActor { - server_config: Option, - generation: Generation, - webhook_actor_addr: Option>, - fs: FileSystem, - net: Network, - repository_factory: Box, - sleep_duration: std::time::Duration, - repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr>, - - // testing - message_log: Option>>>, -} -impl Actor for ServerActor { - type Context = Context; -} - -impl ServerActor { - pub fn new( - fs: FileSystem, - net: Network, - repo: Box, - sleep_duration: std::time::Duration, - ) -> Self { - let generation = Generation::default(); - Self { - server_config: None, - generation, - webhook_actor_addr: None, - fs, - net, - repository_factory: repo, - sleep_duration, - repo_actors: BTreeMap::new(), - message_log: None, - } - } - fn create_forge_data_directories( - &self, - server_config: &ServerConfig, - server_dir: &std::path::Path, - ) -> Result<()> { - for (forge_name, _forge_config) in server_config.forges() { - let forge_dir: PathBuf = (&forge_name).into(); - let path = server_dir.join(&forge_dir); - if self.fs.path_exists(&path)? { - if !self.fs.path_is_dir(&path)? { - return Err(Error::ForgeDirIsNotDirectory { path }); - } - } else { - info!(%forge_name, ?path, "creating storage"); - self.fs.dir_create_all(&path)?; - } - } - - Ok(()) - } - - fn create_forge_repos( - &self, - forge_config: &ForgeConfig, - forge_name: ForgeAlias, - server_storage: &ServerStorage, - webhook: &InboundWebhook, - notify_user_recipient: Recipient, - ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { - let span = - tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); - - let _guard = span.enter(); - info!("Creating Forge"); - let mut repos = vec![]; - let creator = self.create_actor(forge_name, forge_config.clone(), server_storage, webhook); - for (repo_alias, server_repo_config) in forge_config.repos() { - let forge_repo = creator(( - repo_alias, - server_repo_config, - notify_user_recipient.clone(), - )); - info!( - alias = %forge_repo.1, - "Created Repo" - ); - repos.push(forge_repo); - } - repos - } - - fn create_actor( - &self, - forge_name: ForgeAlias, - forge_config: ForgeConfig, - server_storage: &ServerStorage, - webhook: &InboundWebhook, - ) -> impl Fn( - (RepoAlias, &ServerRepoConfig, Recipient), - ) -> (ForgeAlias, RepoAlias, RepoActor) { - let server_storage = server_storage.clone(); - let webhook = webhook.clone(); - let net = self.net.clone(); - let repository_factory = self.repository_factory.duplicate(); - let generation = self.generation; - let sleep_duration = self.sleep_duration; - // let notify_user_recipient = server_addr.recipient(); - move |(repo_alias, server_repo_config, notify_user_recipient)| { - let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); - let _guard = span.enter(); - info!("Creating Repo"); - let gitdir = server_repo_config.gitdir().map_or_else( - || { - GitDir::new( - server_storage - .path() - .join(forge_name.to_string()) - .join(repo_alias.to_string()), - StoragePathType::Internal, - ) - }, - |gitdir| gitdir, - ); - // INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not - // have cloned the repo yet - let repo_details = RepoDetails::new( - generation, - &repo_alias, - server_repo_config, - &forge_name, - &forge_config, - gitdir, - ); - let forge = git_next_forge::Forge::create(repo_details.clone(), net.clone()); - info!("Starting Repo Actor"); - let actor = RepoActor::new( - repo_details, - forge, - webhook.clone(), - generation, - net.clone(), - repository_factory.duplicate(), - sleep_duration, - Some(notify_user_recipient), - ); - (forge_name.clone(), repo_alias, actor) - } - } - - fn start_actor( - &self, - actor: (ForgeAlias, RepoAlias, RepoActor), - ) -> (RepoAlias, Addr) { - let (forge_name, repo_alias, actor) = actor; - let span = tracing::info_span!("start_actor", forge = %forge_name, repo = %repo_alias); - let _guard = span.enter(); - let addr = actor.start(); - addr.do_send(CloneRepo); - info!("Started"); - (repo_alias, addr) - } - - fn server_storage(&self, server_config: &ReceiveServerConfig) -> Option { - let server_storage = server_config.storage().clone(); - let dir = server_storage.path(); - if !dir.exists() { - if let Err(err) = self.fs.dir_create(dir) { - error!(?err, ?dir, "Failed to create server storage"); - return None; - } - } - let Ok(canon) = dir.canonicalize() else { - error!(?dir, "Failed to confirm server storage"); - return None; - }; - if let Err(err) = self.create_forge_data_directories(server_config, &canon) { - error!(?err, "Failure creating forge storage"); - return None; - } - Some(server_storage) - } - - fn do_send(&mut self, msg: M, _ctx: &mut ::Context) - where - M: actix::Message + Send + 'static + std::fmt::Debug, - Self: actix::Handler, - ::Result: Send, - { - if let Some(message_log) = &self.message_log { - let log_message = format!("send: {:?}", msg); - if let Ok(mut log) = message_log.write() { - log.push(log_message); - } - } - #[cfg(not(test))] - _ctx.address().do_send(msg); - tracing::info!("sent"); - } -} +// moved to /crates/cli/src/server/actor