diff --git a/src/server/config/load.rs b/src/server/config/load.rs index 3b47c033..4a7a8317 100644 --- a/src/server/config/load.rs +++ b/src/server/config/load.rs @@ -21,7 +21,15 @@ pub struct RepoConfigUnknownError(pub network::StatusCode); pub async fn load( details: &RepoDetails, forge: &gitforge::Forge, -) -> Result> { +) -> Result< + RepoConfig, + OneOf<( + ForgeFileError, + crate::server::config::Error, + toml::de::Error, + RepoConfigValidationErrors, + )>, +> { let contents = forge .file_contents_get(&details.branch, ".git-next.toml") .await diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index c98321bd..5b0823dc 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -17,6 +17,9 @@ pub enum Error { KxIoFs(kxio::fs::Error), TomlDe(toml::de::Error), } +impl std::error::Error for Error {} + +type Result = core::result::Result; /// Mapped from the `git-next-server.toml` file #[derive(Debug, PartialEq, Eq, Deserialize)] @@ -26,7 +29,7 @@ pub struct ServerConfig { forge: HashMap, } impl ServerConfig { - pub(crate) fn load(fs: &FileSystem) -> Result { + pub(crate) fn load(fs: &FileSystem) -> Result { let str = fs.file_read_to_string(&fs.base().join("git-next-server.toml"))?; toml::from_str(&str).map_err(Into::into) } @@ -90,8 +93,8 @@ impl RepoConfig { Self { branches } } // #[cfg(test)] - pub fn load(toml: &str) -> Result { - toml::from_str(toml) + pub fn load(toml: &str) -> Result { + toml::from_str(toml).map_err(Into::into) } pub const fn branches(&self) -> &RepoBranches { @@ -186,7 +189,7 @@ impl Display for ForgeConfig { pub struct ServerRepoConfig { repo: String, branch: String, - gitdir: Option, // TODO: (#71) use this when supplied + gitdir: Option, main: Option, next: Option, dev: Option, @@ -352,18 +355,21 @@ impl RepoDetails { forge_name: &ForgeName, forge_config: &ForgeConfig, server_storage: &ServerStorage, - ) -> Self { - Self { + ) -> Result { + let path_buf = server_repo_config.gitdir.clone().unwrap_or_else(|| { + server_storage + .path + .join(forge_name.to_string()) + .join(name.to_string()) + }); + let path_buf = std::fs::canonicalize(path_buf)?; + let gitdir = GitDir(path_buf); + Ok(Self { repo_alias: name.clone(), repo_path: RepoPath(server_repo_config.repo.clone()), repo_config: server_repo_config.repo_config(), branch: BranchName(server_repo_config.branch.clone()), - gitdir: GitDir( - server_storage - .path - .join(forge_name.to_string()) - .join(name.to_string()), - ), + gitdir, forge: ForgeDetails { forge_name: forge_name.clone(), forge_type: forge_config.forge_type.clone(), @@ -371,7 +377,7 @@ impl RepoDetails { user: forge_config.user(), token: forge_config.token(), }, - } + }) } pub fn origin(&self) -> secrecy::Secret { let repo_details = self; @@ -386,11 +392,11 @@ impl RepoDetails { } #[allow(dead_code)] - pub fn validate_repo(&self) -> Result<(), RepoValidationError> { + pub fn validate_repo(&self) -> ValidationResult<()> { self.gitdir.validate(self) } - pub fn find_default_push_remote(&self) -> Result { + pub fn find_default_push_remote(&self) -> ValidationResult { let repository = gix::open(self.gitdir.clone()) .map_err(|e| RepoValidationError::UnableToOpenRepo(e.to_string()))?; let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else { @@ -431,6 +437,7 @@ impl Display for RepoDetails { } } +type ValidationResult = core::result::Result; #[derive(Debug)] pub enum RepoValidationError { NoDefaultPushRemote, @@ -443,6 +450,7 @@ pub enum RepoValidationError { configured: GitRemote, }, } +impl std::error::Error for RepoValidationError {} impl Display for RepoValidationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -484,12 +492,12 @@ impl GitDir { pub(crate) fn new(pathbuf: &std::path::Path) -> Self { Self(pathbuf.to_path_buf()) } - #[allow(dead_code)] // TODO: + pub const fn pathbuf(&self) -> &PathBuf { &self.0 } - pub fn validate(&self, repo_details: &RepoDetails) -> Result<(), RepoValidationError> { + pub fn validate(&self, repo_details: &RepoDetails) -> ValidationResult<()> { let configured = repo_details.git_remote(); let found = repo_details.find_default_push_remote()?; if configured != found { diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index 1c03a95d..663e5490 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -7,8 +7,10 @@ use kxio::fs; use super::*; +type Result = core::result::Result>; + #[test] -fn load_should_parse_server_config() -> Result<(), crate::server::config::Error> { +fn load_should_parse_server_config() -> Result<()> { let fs = fs::temp()?; fs.file_write( &fs.base().join("git-next-server.toml"), @@ -111,7 +113,7 @@ fn load_should_parse_server_config() -> Result<(), crate::server::config::Error> } #[test] -fn test_repo_config_load() -> Result<(), crate::server::config::Error> { +fn test_repo_config_load() -> Result<()> { let toml = r#" [branches] main = "main" @@ -150,7 +152,7 @@ fn gitdir_should_display_as_pathbuf() { // NOTE: this test assumes it is being run in a cloned worktree from the project's home repo: // 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<(), RepoValidationError> { +fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?; let mut repo_details = common::repo_details( 1, @@ -172,7 +174,7 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<(), Re } #[test] -fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<(), RepoValidationError> { +fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> { let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?; let mut repo_details = common::repo_details( 1, diff --git a/src/server/gitforge/forgejo/mod.rs b/src/server/gitforge/forgejo/mod.rs index d078e1c3..cee80b52 100644 --- a/src/server/gitforge/forgejo/mod.rs +++ b/src/server/gitforge/forgejo/mod.rs @@ -102,13 +102,13 @@ impl super::ForgeLike for ForgeJoEnv { fn repo_clone(&self, gitdir: GitDir) -> Result<(), RepoCloneError> { if gitdir.exists() { - info!(?gitdir, "Gitdir already exists - validating..."); + info!(%gitdir, "Gitdir already exists - validating..."); gitdir .validate(&self.repo_details) .map_err(|e| RepoCloneError::Validation(e.to_string())) - .inspect(|_| info!(?gitdir, "Validation - OK")) + .inspect(|_| info!(%gitdir, "Validation - OK")) } else { - info!(?gitdir, "Gitdir doesn't exists - cloning..."); + info!(%gitdir, "Gitdir doesn't exists - cloning..."); repo::clone(&self.repo_details, gitdir).inspect(|_| info!("Cloned - OK")) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 168b34b2..4c1ef43c 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -27,7 +27,11 @@ pub enum Error { FailedToCreateDataDirectory(kxio::fs::Error), #[display("The forge data path is not a directory: {path:?}")] - ForgeDirIsNotDirectory { path: PathBuf }, + ForgeDirIsNotDirectory { + path: PathBuf, + }, + + Config(crate::server::config::Error), } type Result = core::result::Result; @@ -81,14 +85,25 @@ pub async fn start(fs: FileSystem, net: Network) { return; } - server_config - .forges() - .flat_map(|(forge_name, forge_config)| { - create_forge_repos(forge_config, forge_name, server_storage, webhook, &net) - }) - .map(start_actor) - .map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient())) - .for_each(|msg| webhook_router.do_send(msg)); + for (forge_name, forge_config) in server_config.forges() { + if let Err(err) = create_forge_repos( + forge_config, + forge_name.clone(), + server_storage, + webhook, + &net, + ) + .map(|repos| { + repos + .into_iter() + .map(start_actor) + .map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient())) + .for_each(|msg| webhook_router.do_send(msg)); + }) { + error!(?err, ?forge_name, "Failed to create forge repo actor"); + return; + } + } let webhook_server = webhook::WebhookActor::new(webhook_router.recipient()).start(); let _ = actix_rt::signal::ctrl_c().await; info!("Ctrl-C received, shutting down..."); @@ -121,20 +136,22 @@ fn create_forge_repos( server_storage: &ServerStorage, webhook: &Webhook, net: &Network, -) -> Vec<(ForgeName, RepoAlias, RepoActor)> { +) -> Result> { let span = tracing::info_span!("Forge", %forge_name, %forge_config); let _guard = span.enter(); info!("Creating Forge"); - forge_config - .repos() - .map(create_actor( - forge_name, - forge_config.clone(), - server_storage, - webhook, - net, - )) - .collect::>() + let mut repos = vec![]; + let creator = create_actor( + forge_name, + forge_config.clone(), + server_storage, + webhook, + net, + ); + for (repo_alias, server_repo_config) in forge_config.repos() { + repos.push(creator((repo_alias, server_repo_config))?); + } + Ok(repos) } fn create_actor( @@ -143,7 +160,7 @@ fn create_actor( server_storage: &ServerStorage, webhook: &Webhook, net: &Network, -) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> (ForgeName, RepoAlias, RepoActor) { +) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> Result<(ForgeName, RepoAlias, RepoActor)> { let server_storage = server_storage.clone(); let webhook = webhook.clone(); let net = net.clone(); @@ -151,19 +168,16 @@ fn create_actor( let span = tracing::info_span!("Repo", %repo_name, %server_repo_config); let _guard = span.enter(); info!("Creating Repo"); - let actor = actors::repo::RepoActor::new( - config::RepoDetails::new( - &repo_name, - server_repo_config, - &forge_name, - &forge_config, - &server_storage, - ), - webhook.clone(), - net.clone(), - ); + let repo_details = config::RepoDetails::new( + &repo_name, + server_repo_config, + &forge_name, + &forge_config, + &server_storage, + )?; + let actor = actors::repo::RepoActor::new(repo_details, webhook.clone(), net.clone()); info!("Created Repo"); - (forge_name.clone(), repo_name, actor) + Ok((forge_name.clone(), repo_name, actor)) } }