| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | use std::path::Path; |
| 9 | use std::process::{Command, ExitCode}; |
| 10 | |
| 11 | use config::Config; |
| 12 | |
| 13 | use crate::context::block_on; |
| 14 | |
| 15 | |
| 16 | const MIN_GIT: (u32, u32) = (2, 41); |
| 17 | |
| 18 | |
| 19 | pub(crate) fn migrate(config_path: Option<&Path>) -> anyhow::Result<ExitCode> { |
| 20 | block_on(async { |
| 21 | let (_config, _store) = crate::context::open_store(config_path).await?; |
| 22 | |
| 23 | println!("migrations applied"); |
| 24 | anyhow::Ok(ExitCode::SUCCESS) |
| 25 | }) |
| 26 | } |
| 27 | |
| 28 | |
| 29 | |
| 30 | |
| 31 | |
| 32 | |
| 33 | pub(crate) fn doctor(config_path: Option<&Path>, quiet: bool) -> anyhow::Result<ExitCode> { |
| 34 | let mut ok = true; |
| 35 | let mut check = |label: &str, result: Result<String, String>| { |
| 36 | match &result { |
| 37 | Ok(detail) => { |
| 38 | if !quiet { |
| 39 | println!("ok {label}: {detail}"); |
| 40 | } |
| 41 | } |
| 42 | Err(detail) => { |
| 43 | ok = false; |
| 44 | |
| 45 | eprintln!("FAIL {label}: {detail}"); |
| 46 | } |
| 47 | } |
| 48 | }; |
| 49 | |
| 50 | |
| 51 | let loaded = match config::load(config_path) { |
| 52 | Ok(loaded) => loaded, |
| 53 | Err(err) => { |
| 54 | eprintln!("FAIL config: {err}"); |
| 55 | return Ok(ExitCode::FAILURE); |
| 56 | } |
| 57 | }; |
| 58 | check("config", Ok("loaded".to_string())); |
| 59 | |
| 60 | check("git", check_git(&loaded.config)); |
| 61 | check("storage", Ok(describe_storage(&loaded.config))); |
| 62 | let database = block_on(async { anyhow::Ok(check_database(&loaded.config).await) })?; |
| 63 | check("database", database); |
| 64 | |
| 65 | if ok { |
| 66 | if !quiet { |
| 67 | println!("all checks passed"); |
| 68 | } |
| 69 | Ok(ExitCode::SUCCESS) |
| 70 | } else { |
| 71 | Ok(ExitCode::FAILURE) |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | |
| 76 | fn describe_storage(config: &Config) -> String { |
| 77 | match config.storage.backend { |
| 78 | config::StorageBackend::Local => { |
| 79 | format!("local ({})", config.storage.data_dir.display()) |
| 80 | } |
| 81 | config::StorageBackend::S3 => match &config.storage.s3 { |
| 82 | Some(s3) => { |
| 83 | let endpoint = s3.endpoint.as_deref().unwrap_or("aws"); |
| 84 | format!( |
| 85 | "s3 (bucket {}, region {}, {endpoint})", |
| 86 | s3.bucket, s3.region |
| 87 | ) |
| 88 | } |
| 89 | None => "s3 (unconfigured)".to_string(), |
| 90 | }, |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | |
| 95 | fn check_git(config: &Config) -> Result<String, String> { |
| 96 | let output = Command::new(&config.git.binary) |
| 97 | .arg("--version") |
| 98 | .output() |
| 99 | .map_err(|err| format!("cannot run {:?}: {err}", config.git.binary))?; |
| 100 | if !output.status.success() { |
| 101 | return Err(format!("{:?} --version failed", config.git.binary)); |
| 102 | } |
| 103 | let text = String::from_utf8_lossy(&output.stdout); |
| 104 | let version = parse_git_version(&text) |
| 105 | .ok_or_else(|| format!("could not parse git version from {text:?}"))?; |
| 106 | if version < MIN_GIT { |
| 107 | return Err(format!( |
| 108 | "git {}.{} is too old; need >= {}.{}", |
| 109 | version.0, version.1, MIN_GIT.0, MIN_GIT.1 |
| 110 | )); |
| 111 | } |
| 112 | Ok(format!("{}.{} ({})", version.0, version.1, text.trim())) |
| 113 | } |
| 114 | |
| 115 | |
| 116 | fn parse_git_version(text: &str) -> Option<(u32, u32)> { |
| 117 | let version = text.split_whitespace().nth(2)?; |
| 118 | let mut parts = version.split('.'); |
| 119 | let major = parts.next()?.parse().ok()?; |
| 120 | let minor = parts.next()?.parse().ok()?; |
| 121 | Some((major, minor)) |
| 122 | } |
| 123 | |
| 124 | |
| 125 | async fn check_database(config: &Config) -> Result<String, String> { |
| 126 | let store = store::Store::connect(&config.database.url, config.database.max_connections) |
| 127 | .await |
| 128 | .map_err(|err| format!("connect failed: {err}"))?; |
| 129 | store |
| 130 | .migrate() |
| 131 | .await |
| 132 | .map_err(|err| format!("migrate failed: {err}"))?; |
| 133 | Ok(format!("{:?} reachable", store.backend())) |
| 134 | } |
| 135 | |
| 136 | #[cfg(test)] |
| 137 | mod tests { |
| 138 | #![allow(clippy::unwrap_used)] |
| 139 | |
| 140 | use super::*; |
| 141 | |
| 142 | #[test] |
| 143 | fn parses_git_version() { |
| 144 | assert_eq!(parse_git_version("git version 2.43.0"), Some((2, 43))); |
| 145 | assert_eq!( |
| 146 | parse_git_version("git version 2.41.0.windows.1"), |
| 147 | Some((2, 41)) |
| 148 | ); |
| 149 | assert_eq!(parse_git_version("nonsense"), None); |
| 150 | } |
| 151 | } |