//! forge: binary entry point. Dispatch: serve (default) | hook | runner ... | admin create-user , admin set-username , admin reset-password . //! //! `runner` is the forge CI runner, the same command line as the standalone `forge-runner` binary. //! The hidden `__sandbox` helper is dispatched here too, because the runner starts it as this //! executable. #![recursion_limit = "256"] mod access; mod admin_cli; mod admin_client; mod advisories; mod api; mod audit; mod auth; mod backup; mod blobs; mod boot; mod ci; mod client_ip; mod compression; mod git_auth; mod git_http; mod hook_cmd; mod i18n; mod internal; mod lfs; mod lfs_pointer; mod mail; mod mirrors; mod notify; mod notify_hub; mod pages; mod post_update; mod pulls_sync; mod raw; mod render; mod replication; mod self_update; mod signatures; mod ssh_backend; mod state; mod templates; #[cfg(test)] mod test_support; mod theme; mod throttle; mod views; mod webhooks; use std::path::PathBuf; use std::process::ExitCode; use forge_core::Config; use tracing_subscriber::EnvFilter; use topcoat::router::route; type BoxError = Box; const USAGE: &str = "usage: forge [--config ] [serve | hook | runner ... | admin create-user [--admin] | admin set-username | admin reset-password | admin trash-list | admin trash-purge --older-than-days | admin audit-purge --older-than-days | admin delete-user [--yes] | admin backup --out [--include-key] [--repos-only | --db-only] | admin backup-verify | admin fsck [--full] | admin lfs-gc | admin restore --from --data-dir ]"; const DEFAULT_CONFIG: &str = "config/forge.toml"; #[route(GET "/healthz")] async fn healthz() -> Result { Ok(format!("ok {}", env!("FORGE_BUILD_SHA"))) } async fn serve(config_path: &std::path::Path) -> Result<(), BoxError> { let config = Config::load(config_path)?; boot::serve(config).await?; Ok(()) } /// Splits the raw args into the config path, the `--admin` flag and the positional words. fn parse_args( args: impl Iterator, ) -> Result<(Option, bool, Vec), String> { let mut config = None; let mut admin = false; let mut positional = Vec::new(); let mut args = args; while let Some(arg) = args.next() { match arg.as_str() { "--config" => { let path = args.next().ok_or("--config needs a path")?; config = Some(PathBuf::from(path)); } "--admin" => admin = true, _ => positional.push(arg), } } Ok((config, admin, positional)) } fn main() -> ExitCode { // The sandbox helpers must start before any runtime or subscriber exists. if forge_runner::cli::is_sandbox_invocation() { std::process::exit(forge_runner::cli::hidden_main()); } // `runner` runs before the forge config is parsed, because its `--config` means the runner // config, not the forge config. let raw: Vec = std::env::args_os().skip(1).collect(); if raw.first().is_some_and(|arg| arg == "runner") { return forge_runner::cli::main_with_args("forge runner", raw.into_iter().skip(1)); } let runtime = match tokio::runtime::Builder::new_multi_thread() .enable_all() .build() { Ok(runtime) => runtime, Err(e) => { eprintln!("forge: cannot start the runtime: {e}"); return ExitCode::FAILURE; } }; runtime.block_on(real_main()) } async fn real_main() -> ExitCode { // Logs go to stderr: the hook stages write protocol output to stdout. let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt() .with_env_filter(filter) .with_writer(std::io::stderr) .init(); let (flag_config, admin, positional) = match parse_args(std::env::args().skip(1)) { Ok(parsed) => parsed, Err(e) => { eprintln!("forge: {e}\n{USAGE}"); return ExitCode::from(2); } }; let config_path = flag_config .or_else(|| std::env::var_os("FORGE_CONFIG").map(PathBuf::from)) .unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG)); let parts: Vec<&str> = positional.iter().map(String::as_str).collect(); let outcome: Result<(), BoxError> = match parts.as_slice() { [] | ["serve"] => serve(&config_path).await, ["--version"] => { println!( "forge {} {}", env!("CARGO_PKG_VERSION"), env!("FORGE_BUILD_SHA") ); return ExitCode::SUCCESS; } ["hook", stage] => hook_cmd::run(stage).await.map_err(Into::into), ["admin", "create-user", username] => admin_cli::create_user(&config_path, username, admin) .await .map_err(Into::into), ["admin", "reset-password", username] => admin_cli::reset_password(&config_path, username) .await .map_err(Into::into), ["admin", "set-username", current, new_display] => { admin_cli::set_username(&config_path, current, new_display) .await .map_err(Into::into) } ["admin", "backup", rest @ ..] => match admin_cli::parse_backup_args(rest) { Ok(opts) => admin_cli::backup(&config_path, opts) .await .map_err(Into::into), Err(e) => { eprintln!("forge: {e}\n{USAGE}"); return ExitCode::from(2); } }, ["admin", "restore", rest @ ..] => match admin_cli::parse_restore_args(rest) { Ok((from, data_dir)) => admin_cli::restore(&from, &data_dir) .await .map_err(Into::into), Err(e) => { eprintln!("forge: {e}\n{USAGE}"); return ExitCode::from(2); } }, ["admin", "fsck"] => admin_cli::fsck(&config_path, false) .await .map_err(Into::into), ["admin", "fsck", "--full"] => admin_cli::fsck(&config_path, true) .await .map_err(Into::into), ["admin", "lfs-gc"] => admin_cli::lfs_gc(&config_path).await.map_err(Into::into), ["admin", "backup-verify", path] => admin_cli::backup_verify(std::path::Path::new(path)) .await .map_err(Into::into), ["admin", "trash-list"] => admin_cli::trash_list(&config_path).map_err(Into::into), ["admin", "delete-user", username] => admin_cli::delete_user(&config_path, username, false) .await .map_err(Into::into), ["admin", "delete-user", username, "--yes"] => { admin_cli::delete_user(&config_path, username, true) .await .map_err(Into::into) } ["admin", "audit-purge", "--older-than-days", days] => match days.parse::() { Ok(days) => admin_cli::audit_purge(&config_path, days) .await .map_err(Into::into), Err(_) => { eprintln!("forge: --older-than-days needs a whole number of days\n{USAGE}"); return ExitCode::from(2); } }, ["admin", "trash-purge", "--older-than-days", days] => match days.parse::() { Ok(days) => admin_cli::trash_purge(&config_path, days).map_err(Into::into), Err(_) => { eprintln!("forge: --older-than-days needs a whole number of days\n{USAGE}"); return ExitCode::from(2); } }, _ => { eprintln!("{USAGE}"); return ExitCode::from(2); } }; match outcome { // A self-update swap asks for a restart. systemd Restart=on-failure starts the new binary. Ok(()) if self_update::restart_pending() => ExitCode::from(self_update::RESTART_EXIT_CODE), Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("forge: {e}"); ExitCode::FAILURE } } }