Author:
Hash:
Timestamp:
+504 -539 +/-30 browse
Kevin Schoon [me@kevinschoon.com]
86b027974fd2a875243fc21601c87f9e6353278f
Tue, 23 Jun 2026 18:45:18 +0000 (3 weeks ago)
| 1 | diff --git a/ayllu-build/src/config.rs b/ayllu-build/src/config.rs |
| 2 | index 7b25ff7..4245ca1 100644 |
| 3 | --- a/ayllu-build/src/config.rs |
| 4 | +++ b/ayllu-build/src/config.rs |
| 5 | @@ -79,36 +79,36 @@ pub struct Build { |
| 6 | |
| 7 | impl Build { |
| 8 | fn default_work_dir() -> PathBuf { |
| 9 | - Self::default().work_dir |
| 10 | + data_dir().join("build") |
| 11 | } |
| 12 | |
| 13 | fn default_oci_binary() -> PathBuf { |
| 14 | - Self::default().oci_binary |
| 15 | + PathBuf::from("/usr/bin/crun") |
| 16 | } |
| 17 | |
| 18 | fn default_interrupt_timeout() -> u64 { |
| 19 | - Self::default().interrupt_timeout |
| 20 | + crate::handle::DEFAULT_INTERRUPT_TIMEOUT_SECS |
| 21 | } |
| 22 | |
| 23 | fn default_init_script() -> String { |
| 24 | - Self::default().init_script |
| 25 | + include_str!("../init.tmpl.sh").to_string() |
| 26 | } |
| 27 | |
| 28 | fn default_runtime_dir() -> PathBuf { |
| 29 | - Self::default().runtime_dir |
| 30 | + ayllu_config::runtime_dir().join("ayllu-build") |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | impl Default for Build { |
| 35 | fn default() -> Self { |
| 36 | Self { |
| 37 | - work_dir: data_dir().join("build"), |
| 38 | - pre_processor: Default::default(), |
| 39 | + work_dir: Build::default_work_dir(), |
| 40 | + pre_processor: None, |
| 41 | + oci_binary: Build::default_oci_binary(), |
| 42 | images: HashMap::default(), |
| 43 | - oci_binary: Path::new("/usr/bin/crun").to_path_buf(), |
| 44 | - interrupt_timeout: crate::handle::DEFAULT_INTERRUPT_TIMEOUT_SECS, |
| 45 | - init_script: include_str!("../init.tmpl.sh").to_string(), |
| 46 | - runtime_dir: ayllu_config::runtime_dir().join("ayllu-build"), |
| 47 | + interrupt_timeout: Build::default_interrupt_timeout(), |
| 48 | + init_script: Build::default_init_script(), |
| 49 | + runtime_dir: Build::default_runtime_dir(), |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | @@ -134,3 +134,16 @@ pub fn load(path: Option<&Path>) -> Result<Config, Error> { |
| 54 | Err(e) => Err(e), |
| 55 | } |
| 56 | } |
| 57 | + |
| 58 | + #[cfg(test)] |
| 59 | + mod test { |
| 60 | + use ayllu_config::{EXAMPLE_CONFIG, Reader}; |
| 61 | + |
| 62 | + use crate::config::Config; |
| 63 | + |
| 64 | + #[test] |
| 65 | + fn check_config() { |
| 66 | + Reader::<Config>::read(EXAMPLE_CONFIG).unwrap(); |
| 67 | + Reader::<Config>::read("").unwrap(); |
| 68 | + } |
| 69 | + } |
| 70 | diff --git a/ayllu-keys/src/main.rs b/ayllu-keys/src/main.rs |
| 71 | index 7b16ffb..3c4b97b 100644 |
| 72 | --- a/ayllu-keys/src/main.rs |
| 73 | +++ b/ayllu-keys/src/main.rs |
| 74 | @@ -90,6 +90,7 @@ fn main() -> ExitCode { |
| 75 | #[cfg(test)] |
| 76 | mod test { |
| 77 | |
| 78 | + use ayllu_config::{EXAMPLE_CONFIG, Reader}; |
| 79 | use ayllu_identity::{PublicKey, WrappedKey}; |
| 80 | |
| 81 | use super::*; |
| 82 | @@ -117,4 +118,10 @@ mod test { |
| 83 | .is_some_and(|identity| { identity.username == "hello" }) |
| 84 | ); |
| 85 | } |
| 86 | + |
| 87 | + #[test] |
| 88 | + fn check_config() { |
| 89 | + Reader::<Config>::read(EXAMPLE_CONFIG).unwrap(); |
| 90 | + Reader::<Config>::read("").unwrap(); |
| 91 | + } |
| 92 | } |
| 93 | diff --git a/ayllu-migrate/src/main.rs b/ayllu-migrate/src/main.rs |
| 94 | index c80817f..b822eea 100644 |
| 95 | --- a/ayllu-migrate/src/main.rs |
| 96 | +++ b/ayllu-migrate/src/main.rs |
| 97 | @@ -3,7 +3,7 @@ use ayllu_config::{Configurable, Database}; |
| 98 | use serde::{Deserialize, Serialize}; |
| 99 | use tracing::Level; |
| 100 | |
| 101 | - #[derive(Deserialize, Serialize)] |
| 102 | + #[derive(Deserialize, Serialize, Default)] |
| 103 | pub struct Config { |
| 104 | #[serde(default = "Database::default")] |
| 105 | pub database: Database, |
| 106 | diff --git a/ayllu-shell/src/config.rs b/ayllu-shell/src/config.rs |
| 107 | index 84d644d..920fb73 100644 |
| 108 | --- a/ayllu-shell/src/config.rs |
| 109 | +++ b/ayllu-shell/src/config.rs |
| 110 | @@ -10,6 +10,9 @@ use serde::{Deserialize, Serialize}; |
| 111 | pub struct Shell { |
| 112 | /// Message of the Day displayed for each interactive Ayllu session |
| 113 | pub motd: String, |
| 114 | + /// Path to a socket for communicating with ayllu-buld |
| 115 | + #[serde(default = "Shell::default_build_socket")] |
| 116 | + pub build_socket: PathBuf, |
| 117 | /// Path to remove removed repositories |
| 118 | #[serde(default = "Shell::default_trash_path")] |
| 119 | pub trash_path: PathBuf, |
| 120 | @@ -19,48 +22,35 @@ impl Shell { |
| 121 | fn default_trash_path() -> PathBuf { |
| 122 | Path::new("/usr/share/ayllu/.trash").to_path_buf() |
| 123 | } |
| 124 | + |
| 125 | + fn default_build_socket() -> PathBuf { |
| 126 | + ayllu_config::runtime_dir().join("ayllu/build.socket") |
| 127 | + } |
| 128 | } |
| 129 | |
| 130 | - #[derive(Serialize, Deserialize, Clone)] |
| 131 | + #[derive(Serialize, Deserialize, Clone, Default)] |
| 132 | pub struct Config { |
| 133 | - #[serde(default = "Config::default_log_level")] |
| 134 | - pub log_level: String, |
| 135 | - #[serde(default = "Config::default_branch")] |
| 136 | - pub default_branch: String, |
| 137 | - pub base_dir: Option<PathBuf>, |
| 138 | - #[serde(default = "Vec::new")] |
| 139 | - pub identities: Vec<Identity>, |
| 140 | + #[serde(flatten)] |
| 141 | + pub common: ayllu_config::Common, |
| 142 | #[serde(default = "Shell::default")] |
| 143 | pub shell: Shell, |
| 144 | #[serde(default = "Vec::new")] |
| 145 | pub collections: Vec<Collection>, |
| 146 | - #[serde(default = "Config::default_build_socket")] |
| 147 | - pub build_socket_path: PathBuf, |
| 148 | + #[serde(default = "Vec::new")] |
| 149 | + pub identities: Vec<Identity>, |
| 150 | } |
| 151 | |
| 152 | - impl Config { |
| 153 | - fn default_log_level() -> String { |
| 154 | - String::from("INFO") |
| 155 | - } |
| 156 | + impl Configurable for Config {} |
| 157 | |
| 158 | - fn default_branch() -> String { |
| 159 | - ayllu_git::DEFAULT_GIT_BRANCH.to_string() |
| 160 | - } |
| 161 | + #[cfg(test)] |
| 162 | + mod test { |
| 163 | + use ayllu_config::{EXAMPLE_CONFIG, Reader}; |
| 164 | |
| 165 | - fn default_build_socket() -> PathBuf { |
| 166 | - ayllu_config::runtime_dir().join("ayllu/build.socket") |
| 167 | - } |
| 168 | - } |
| 169 | + use crate::config::Config; |
| 170 | |
| 171 | - impl Configurable for Config { |
| 172 | - fn initialize(&mut self) -> Result<(), ayllu_config::Error> { |
| 173 | - if let Some(base_dir) = &self.base_dir { |
| 174 | - self.collections.iter_mut().for_each(|collection| { |
| 175 | - if collection.path.is_relative() { |
| 176 | - collection.path = base_dir.join(&collection.path); |
| 177 | - }; |
| 178 | - }); |
| 179 | - }; |
| 180 | - Ok(()) |
| 181 | + #[test] |
| 182 | + fn check_config() { |
| 183 | + Reader::<Config>::read(EXAMPLE_CONFIG).unwrap(); |
| 184 | + Reader::<Config>::read("").unwrap(); |
| 185 | } |
| 186 | } |
| 187 | diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs |
| 188 | index 747c3b8..0a53b3a 100644 |
| 189 | --- a/ayllu-shell/src/main.rs |
| 190 | +++ b/ayllu-shell/src/main.rs |
| 191 | @@ -16,7 +16,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> { |
| 192 | .find(|identity| identity.username == username); |
| 193 | match args.command { |
| 194 | Some(Subcommand::GitReceivePack { path }) => { |
| 195 | - let resolved = if let Some(base_dir) = config.base_dir { |
| 196 | + let resolved = if let Some(base_dir) = config.common.base_path { |
| 197 | let trimmed = if path.is_absolute() { |
| 198 | path.strip_prefix("/").unwrap() |
| 199 | } else { |
| 200 | @@ -36,7 +36,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> { |
| 201 | if ayllu_git::git_dir(&resolved).is_ok_and(|is_git_dir| is_git_dir) { |
| 202 | Wrapper::new(&resolved)? |
| 203 | } else { |
| 204 | - Wrapper::create(&resolved, Some(&config.default_branch), true)? |
| 205 | + Wrapper::create(&resolved, Some(&config.common.default_branch), true)? |
| 206 | }; |
| 207 | repository.receive_pack()?; |
| 208 | } |
| 209 | @@ -47,7 +47,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> { |
| 210 | } |
| 211 | Some(Subcommand::GitUploadPack { ref path }) => { |
| 212 | let resolved = if path.is_relative() { |
| 213 | - if let Some(base_dir) = config.base_dir { |
| 214 | + if let Some(base_dir) = config.common.base_path.as_ref() { |
| 215 | &base_dir.join(path) |
| 216 | } else { |
| 217 | path |
| 218 | diff --git a/ayllu-shell/src/ui/repository_build.rs b/ayllu-shell/src/ui/repository_build.rs |
| 219 | index 186426e..56ac52c 100644 |
| 220 | --- a/ayllu-shell/src/ui/repository_build.rs |
| 221 | +++ b/ayllu-shell/src/ui/repository_build.rs |
| 222 | @@ -92,7 +92,7 @@ impl MenuItem for Item { |
| 223 | ) { |
| 224 | self.0.collection(choice.collection()); |
| 225 | self.0.repository(choice.clone()); |
| 226 | - self.0.socket_path(config.build_socket_path.clone()); |
| 227 | + self.0.socket_path(config.shell.build_socket.clone()); |
| 228 | Ok(Some(Action::Branch)) |
| 229 | } else { |
| 230 | Ok(None) |
| 231 | diff --git a/ayllu-web/src/config.rs b/ayllu-web/src/config.rs |
| 232 | index 618a57a..40bd162 100644 |
| 233 | --- a/ayllu-web/src/config.rs |
| 234 | +++ b/ayllu-web/src/config.rs |
| 235 | @@ -4,7 +4,7 @@ use std::path::Path; |
| 236 | use std::{collections::HashMap, path::PathBuf}; |
| 237 | use url::Url; |
| 238 | |
| 239 | - use ayllu_config::{Configurable, Database, Error}; |
| 240 | + use ayllu_config::{Common, Configurable, Database, Error}; |
| 241 | use ayllu_git::Collection; |
| 242 | use ayllu_identity::Identity; |
| 243 | |
| 244 | @@ -78,24 +78,62 @@ pub struct Theme { |
| 245 | |
| 246 | #[derive(Deserialize, Serialize, Clone, Debug)] |
| 247 | pub struct Web { |
| 248 | + pub origin: Url, |
| 249 | + #[serde(default = "Web::default_site_name")] |
| 250 | + pub site_name: String, |
| 251 | + #[serde(default = "Web::default_address")] |
| 252 | + pub address: String, |
| 253 | pub extra_css: Option<String>, |
| 254 | - #[serde(default = "Web::default_default_theme")] |
| 255 | + #[serde(default = "Web::default_theme")] |
| 256 | pub default_theme: String, |
| 257 | - #[serde(default = "Web::default_themes")] |
| 258 | - pub themes: Vec<Theme>, |
| 259 | pub additional_themes: Option<Vec<Theme>>, |
| 260 | #[serde(default = "Web::default_unsafe_markdown")] |
| 261 | pub unsafe_markdown: bool, |
| 262 | + #[serde(default = "Git::default")] |
| 263 | + pub git: Git, |
| 264 | + /// If man pages should be hosted on the site |
| 265 | + #[serde(default = "Web::default_serve_docs")] |
| 266 | + pub serve_docs: bool, |
| 267 | + #[serde(default = "Web::default_about_blurb")] |
| 268 | + pub blurb: String, |
| 269 | + #[serde(default = "Web::default_robots_txt")] |
| 270 | + pub robots: String, |
| 271 | + #[serde(default = "default_false")] |
| 272 | + pub subpath_mode: bool, |
| 273 | + pub lfs: Option<Lfs>, |
| 274 | + #[serde(default = "Sites::default")] |
| 275 | + pub sites: Sites, |
| 276 | + #[serde(rename = "tree-sitter")] |
| 277 | + pub tree_sitter: Option<TreeSitter>, |
| 278 | + #[serde(default = "Vec::default")] |
| 279 | + pub languages: Vec<Language>, |
| 280 | + #[serde(default = "Web::default_themes")] |
| 281 | + pub themes: Vec<Theme>, |
| 282 | + #[serde(default = "Web::default_rss_time_to_live")] |
| 283 | + pub rss_time_to_live: i64, |
| 284 | } |
| 285 | |
| 286 | impl Default for Web { |
| 287 | fn default() -> Self { |
| 288 | Self { |
| 289 | + origin: Web::default_origin(), |
| 290 | + site_name: Web::default_site_name(), |
| 291 | + address: Web::default_address(), |
| 292 | extra_css: None, |
| 293 | - default_theme: Web::default_default_theme(), |
| 294 | - themes: Web::default_themes(), |
| 295 | + default_theme: Web::default_theme(), |
| 296 | additional_themes: None, |
| 297 | - unsafe_markdown: Web::default_unsafe_markdown(), |
| 298 | + unsafe_markdown: true, |
| 299 | + git: Git::default(), |
| 300 | + serve_docs: false, |
| 301 | + blurb: Web::default_about_blurb(), |
| 302 | + robots: Web::default_robots_txt(), |
| 303 | + subpath_mode: false, |
| 304 | + lfs: None, |
| 305 | + sites: Sites::default(), |
| 306 | + tree_sitter: None, |
| 307 | + languages: Vec::default(), |
| 308 | + themes: Web::default_themes(), |
| 309 | + rss_time_to_live: Web::default_rss_time_to_live(), |
| 310 | } |
| 311 | } |
| 312 | } |
| 313 | @@ -129,25 +167,40 @@ impl Web { |
| 314 | ] |
| 315 | } |
| 316 | |
| 317 | - fn default_default_theme() -> String { |
| 318 | + fn default_origin() -> Url { |
| 319 | + Url::parse("http://127.0.0.1:10000").unwrap() |
| 320 | + } |
| 321 | + |
| 322 | + fn default_address() -> String { |
| 323 | + "127.0.0.1:10000".to_string() |
| 324 | + } |
| 325 | + |
| 326 | + fn default_theme() -> String { |
| 327 | String::from("rose-pine") |
| 328 | } |
| 329 | |
| 330 | + fn default_rss_time_to_live() -> i64 { |
| 331 | + 3600 |
| 332 | + } |
| 333 | + |
| 334 | fn default_unsafe_markdown() -> bool { |
| 335 | true |
| 336 | } |
| 337 | - } |
| 338 | |
| 339 | - #[derive(Deserialize, Serialize, Clone, Debug)] |
| 340 | - pub struct Http { |
| 341 | - pub address: String, |
| 342 | - } |
| 343 | + fn default_about_blurb() -> String { |
| 344 | + String::from(DEFAULT_ABOUT_BLURB) |
| 345 | + } |
| 346 | |
| 347 | - impl Default for Http { |
| 348 | - fn default() -> Self { |
| 349 | - Self { |
| 350 | - address: String::from("127.0.0.1:10000"), |
| 351 | - } |
| 352 | + fn default_robots_txt() -> String { |
| 353 | + String::from(DEFAULT_ROBOTS_TXT.trim_start()) |
| 354 | + } |
| 355 | + |
| 356 | + fn default_site_name() -> String { |
| 357 | + String::from("🌄 ayllu") |
| 358 | + } |
| 359 | + |
| 360 | + fn default_serve_docs() -> bool { |
| 361 | + true |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | @@ -193,13 +246,6 @@ pub struct TreeSitter { |
| 366 | impl TreeSitter { |
| 367 | fn validate(&self) -> Result<(), Error> { |
| 368 | Ok(()) |
| 369 | - // self.languages.iter().try_for_each(|language| { |
| 370 | - // let module_path = self.template.module(&self.base_path, language); |
| 371 | - // if !module_path.exists() { |
| 372 | - // return Err(Error::Validation(format!("{module_path:?} does not exist"))); |
| 373 | - // }; |
| 374 | - // Ok(()) |
| 375 | - // }) |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | @@ -240,79 +286,32 @@ pub struct Git { |
| 380 | #[derive(Default, Deserialize, Serialize, Clone, Debug)] |
| 381 | pub struct Build {} |
| 382 | |
| 383 | - #[derive(Deserialize, Serialize, Clone, Debug)] |
| 384 | + #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
| 385 | pub struct Config { |
| 386 | - #[serde(default = "Config::default_site_name")] |
| 387 | - pub site_name: String, |
| 388 | - #[serde(default = "Config::default_origin")] |
| 389 | - pub origin: String, |
| 390 | - // TODO: Deprecate this |
| 391 | - pub domain: Option<String>, |
| 392 | - #[serde(default = "Git::default")] |
| 393 | - pub git: Git, |
| 394 | - pub sysadmin: Option<String>, |
| 395 | - /// If man pages should be hosted on the site |
| 396 | - #[serde(default = "Config::default_serve_docs")] |
| 397 | - pub serve_docs: bool, |
| 398 | - #[serde(default = "Config::default_about_blurb")] |
| 399 | - pub blurb: String, |
| 400 | - #[serde(default = "Config::default_robots_txt")] |
| 401 | - pub robots: String, |
| 402 | - #[serde(default = "default_false")] |
| 403 | - pub subpath_mode: bool, |
| 404 | - #[serde(default = "Config::default_log_level")] |
| 405 | - pub log_level: String, |
| 406 | - pub default_branch: Option<String>, |
| 407 | - pub rss_time_to_live: Option<i64>, |
| 408 | + #[serde(flatten)] |
| 409 | + pub common: Common, |
| 410 | #[serde(default = "Web::default")] |
| 411 | pub web: Web, |
| 412 | - #[serde(default = "Http::default")] |
| 413 | - pub http: Http, |
| 414 | - #[serde(default = "Vec::new")] |
| 415 | - pub collections: Vec<Collection>, |
| 416 | - // Optional base directory to resolve relative collections from |
| 417 | - pub base_dir: Option<PathBuf>, |
| 418 | - #[serde(default = "Sites::default")] |
| 419 | - pub sites: Sites, |
| 420 | - #[serde(rename = "tree-sitter")] |
| 421 | - pub tree_sitter: Option<TreeSitter>, |
| 422 | - #[serde(default = "Vec::default")] |
| 423 | - pub languages: Vec<Language>, |
| 424 | - pub lfs: Option<Lfs>, |
| 425 | - #[serde(default = "Vec::new")] |
| 426 | - pub identities: Vec<Identity>, |
| 427 | + pub collections: Option<Vec<Collection>>, |
| 428 | + pub identities: Option<Vec<Identity>>, |
| 429 | pub database: Option<Database>, |
| 430 | pub build: Option<Build>, |
| 431 | } |
| 432 | |
| 433 | impl Configurable for Config { |
| 434 | fn validate(&mut self) -> Result<(), Error> { |
| 435 | - let parsed_url = Url::parse(&self.origin) |
| 436 | - .map_err(|e| Error::Validation(format!("Cannot parse URL: {}: {e:?}", self.origin)))?; |
| 437 | - self.domain = parsed_url.domain().map(|domain| domain.to_string()); |
| 438 | - |
| 439 | // verify collection names are all valid |
| 440 | - if let Some(collection) = self |
| 441 | - .collections |
| 442 | - .iter() |
| 443 | - .find(|collection| BANNED_COLLECTION_NAMES.contains(&collection.name.as_str())) |
| 444 | - { |
| 445 | + if let Some(collection) = self.collections.as_ref().and_then(|collections| { |
| 446 | + collections |
| 447 | + .iter() |
| 448 | + .find(|collection| BANNED_COLLECTION_NAMES.contains(&collection.name.as_str())) |
| 449 | + }) { |
| 450 | return Err(Error::Validation(format!( |
| 451 | "{} is a reserved name", |
| 452 | collection.name |
| 453 | ))); |
| 454 | }; |
| 455 | |
| 456 | - // validate that all collection directories exist |
| 457 | - for collection in self.collections.iter() { |
| 458 | - if let Err(err) = metadata(collection.path.clone()) { |
| 459 | - return Err(Error::Validation(format!( |
| 460 | - "Failed to load collection {}:\n{err}", |
| 461 | - collection.name |
| 462 | - ))); |
| 463 | - } |
| 464 | - } |
| 465 | - |
| 466 | if let Some(additional_themes) = self.web.additional_themes.as_ref() { |
| 467 | self.web |
| 468 | .themes |
| 469 | @@ -331,21 +330,13 @@ impl Configurable for Config { |
| 470 | ))); |
| 471 | } |
| 472 | |
| 473 | - if let Some(base_dir) = &self.base_dir { |
| 474 | - self.collections.iter_mut().for_each(|collection| { |
| 475 | - if collection.path.is_relative() { |
| 476 | - collection.path = base_dir.join(&collection.path); |
| 477 | - }; |
| 478 | - }); |
| 479 | - }; |
| 480 | - |
| 481 | if self.build.is_some() && self.database.is_none() { |
| 482 | return Err(Error::Validation( |
| 483 | "Builder is enabled but the database in unconfigured".to_string(), |
| 484 | )); |
| 485 | } |
| 486 | |
| 487 | - if let Some(ts_config) = self.tree_sitter.as_ref() { |
| 488 | + if let Some(ts_config) = self.web.tree_sitter.as_ref() { |
| 489 | ts_config.validate()?; |
| 490 | } |
| 491 | |
| 492 | @@ -353,32 +344,6 @@ impl Configurable for Config { |
| 493 | } |
| 494 | } |
| 495 | |
| 496 | - impl Config { |
| 497 | - fn default_robots_txt() -> String { |
| 498 | - String::from(DEFAULT_ROBOTS_TXT.trim_start()) |
| 499 | - } |
| 500 | - |
| 501 | - fn default_site_name() -> String { |
| 502 | - String::from("🌄 ayllu") |
| 503 | - } |
| 504 | - |
| 505 | - fn default_log_level() -> String { |
| 506 | - String::from("info") |
| 507 | - } |
| 508 | - |
| 509 | - fn default_origin() -> String { |
| 510 | - format!("http://{}", Http::default().address) |
| 511 | - } |
| 512 | - |
| 513 | - fn default_about_blurb() -> String { |
| 514 | - String::from(DEFAULT_ABOUT_BLURB) |
| 515 | - } |
| 516 | - |
| 517 | - fn default_serve_docs() -> bool { |
| 518 | - true |
| 519 | - } |
| 520 | - } |
| 521 | - |
| 522 | #[cfg(test)] |
| 523 | mod tests { |
| 524 | use super::*; |
| 525 | @@ -387,10 +352,6 @@ mod tests { |
| 526 | #[test] |
| 527 | fn test_example_config() { |
| 528 | Reader::<Config>::read(EXAMPLE_CONFIG).unwrap(); |
| 529 | - } |
| 530 | - |
| 531 | - #[test] |
| 532 | - fn test_empty_config() { |
| 533 | Reader::<Config>::read("").unwrap(); |
| 534 | } |
| 535 | } |
| 536 | diff --git a/ayllu-web/src/highlight.rs b/ayllu-web/src/highlight.rs |
| 537 | index c8c7ffc..cf20c51 100644 |
| 538 | --- a/ayllu-web/src/highlight.rs |
| 539 | +++ b/ayllu-web/src/highlight.rs |
| 540 | @@ -55,7 +55,7 @@ unsafe fn load_language(path: &str, hint: &Hint) -> Language { |
| 541 | language |
| 542 | } |
| 543 | |
| 544 | - pub struct Loader<'a>(pub &'a crate::config::Config); |
| 545 | + pub struct Loader<'a>(pub &'a crate::config::Web); |
| 546 | |
| 547 | impl Loader<'_> { |
| 548 | pub unsafe fn load(&self) -> Result<(), std::io::Error> { |
| 549 | diff --git a/ayllu-web/src/main.rs b/ayllu-web/src/main.rs |
| 550 | index b6f977f..178babf 100644 |
| 551 | --- a/ayllu-web/src/main.rs |
| 552 | +++ b/ayllu-web/src/main.rs |
| 553 | @@ -37,32 +37,32 @@ async fn main() -> Result<(), Box<dyn Error>> { |
| 554 | } |
| 555 | Subcommand::Serve {} => { |
| 556 | let mut cfg: config::Config = Reader::load(cli.config.as_deref())?; |
| 557 | - if cfg.collections.is_empty() |
| 558 | + if cfg.collections.is_none() |
| 559 | && let Ok(path) = std::env::current_dir() |
| 560 | && ayllu_git::git_dir(path.as_path()).is_ok_and(|is_git_dir| is_git_dir) |
| 561 | { |
| 562 | - cfg.collections.push(Collection { |
| 563 | + cfg.collections = Some(vec![Collection { |
| 564 | name: String::from("Default"), |
| 565 | description: Some(format!("Collection @ {}", path.to_string_lossy())), |
| 566 | path, |
| 567 | hidden: Default::default(), |
| 568 | - }) |
| 569 | + }]); |
| 570 | } |
| 571 | |
| 572 | init_logger( |
| 573 | cli.level |
| 574 | .as_ref() |
| 575 | .map(|level| Level::from_str(level)) |
| 576 | - .unwrap_or(Level::from_str(&cfg.log_level))?, |
| 577 | + .unwrap_or(Level::from_str(&cfg.common.log_level))?, |
| 578 | ); |
| 579 | |
| 580 | unsafe { |
| 581 | - if cfg.tree_sitter.is_some() { |
| 582 | - Loader(&cfg).load()?; |
| 583 | + if cfg.web.tree_sitter.is_some() { |
| 584 | + Loader(&cfg.web).load()?; |
| 585 | } |
| 586 | } |
| 587 | |
| 588 | - LANGUAGE_TABLE.load(&cfg.languages); |
| 589 | + LANGUAGE_TABLE.load(&cfg.web.languages); |
| 590 | web2::serve(&cfg).await?; |
| 591 | Ok(()) |
| 592 | } |
| 593 | diff --git a/ayllu-web/src/web2/middleware/error.rs b/ayllu-web/src/web2/middleware/error.rs |
| 594 | index 209e599..fddbdcf 100644 |
| 595 | --- a/ayllu-web/src/web2/middleware/error.rs |
| 596 | +++ b/ayllu-web/src/web2/middleware/error.rs |
| 597 | @@ -51,7 +51,11 @@ pub async fn middleware( |
| 598 | NotFoundPage { |
| 599 | base: Base { |
| 600 | title: String::from("Resource Not Found"), |
| 601 | - nav_elements: crate::web2::navigation::global("", false, state.serve_docs), |
| 602 | + nav_elements: crate::web2::navigation::global( |
| 603 | + "", |
| 604 | + false, |
| 605 | + state.web.serve_docs, |
| 606 | + ), |
| 607 | logo: DEFAULT_LOGO.to_string(), |
| 608 | ..Default::default() |
| 609 | }, |
| 610 | @@ -68,7 +72,11 @@ pub async fn middleware( |
| 611 | ErrorPage { |
| 612 | base: Base { |
| 613 | title: String::from("Internal Server Error"), |
| 614 | - nav_elements: crate::web2::navigation::global("", false, state.serve_docs), |
| 615 | + nav_elements: crate::web2::navigation::global( |
| 616 | + "", |
| 617 | + false, |
| 618 | + state.web.serve_docs, |
| 619 | + ), |
| 620 | logo: DEFAULT_LOGO.to_string(), |
| 621 | ..Default::default() |
| 622 | }, |
| 623 | diff --git a/ayllu-web/src/web2/middleware/repository.rs b/ayllu-web/src/web2/middleware/repository.rs |
| 624 | index d184f38..197e5df 100644 |
| 625 | --- a/ayllu-web/src/web2/middleware/repository.rs |
| 626 | +++ b/ayllu-web/src/web2/middleware/repository.rs |
| 627 | @@ -47,20 +47,21 @@ impl Preamble { |
| 628 | commitish: Option<String>, |
| 629 | file_path: Option<String>, |
| 630 | ) -> Result<Self, Error> { |
| 631 | - let (collection_path, repo_path, hidden) = match system_config |
| 632 | - .collections |
| 633 | - .iter() |
| 634 | - .find(|collection| collection.name == collection_name) |
| 635 | - { |
| 636 | - Some(collection) => ( |
| 637 | - collection.path.clone(), |
| 638 | - collection.path.join(std::path::Path::new(&repo_name)), |
| 639 | - collection.hidden.is_some_and(|hidden| hidden), |
| 640 | - ), |
| 641 | - None => { |
| 642 | - return Err(Error::NotFound(String::from("collection not found"))); |
| 643 | - } |
| 644 | - }; |
| 645 | + let (collection_path, repo_path, hidden) = |
| 646 | + match system_config.collections.as_ref().and_then(|collections| { |
| 647 | + collections |
| 648 | + .iter() |
| 649 | + .find(|collection| collection.name == collection_name) |
| 650 | + }) { |
| 651 | + Some(collection) => ( |
| 652 | + collection.path.clone(), |
| 653 | + collection.path.join(std::path::Path::new(&repo_name)), |
| 654 | + collection.hidden.is_some_and(|hidden| hidden), |
| 655 | + ), |
| 656 | + None => { |
| 657 | + return Err(Error::NotFound(String::from("collection not found"))); |
| 658 | + } |
| 659 | + }; |
| 660 | let repository = Repository::new(&repo_path)?; |
| 661 | let is_empty = repository.is_empty()?; |
| 662 | let config = repository.config()?; |
| 663 | @@ -73,9 +74,6 @@ impl Preamble { |
| 664 | } else if let Some(default_branch) = config.default_branch.clone() { |
| 665 | let commit = repository.resolve_commit(Some(default_branch.as_str()))?; |
| 666 | (default_branch.clone(), commit) |
| 667 | - } else if let Some(system_default_branch) = &system_config.default_branch { |
| 668 | - let commit = repository.resolve_commit(Some(system_default_branch.as_str()))?; |
| 669 | - (system_default_branch.clone(), commit) |
| 670 | } else { |
| 671 | let default_branch = repository.default_ref()?; |
| 672 | let commit = repository.resolve_commit(Some(default_branch.as_str()))?; |
| 673 | diff --git a/ayllu-web/src/web2/middleware/sites.rs b/ayllu-web/src/web2/middleware/sites.rs |
| 674 | index 7d846c8..e6aab53 100644 |
| 675 | --- a/ayllu-web/src/web2/middleware/sites.rs |
| 676 | +++ b/ayllu-web/src/web2/middleware/sites.rs |
| 677 | @@ -37,19 +37,21 @@ fn repositories(collections: &[Collection]) -> Result<Vec<PathBuf>, Error> { |
| 678 | |
| 679 | pub fn sites(cfg: &Config) -> Result<Sites, Error> { |
| 680 | let mut sites: Vec<(String, (String, String))> = Vec::new(); |
| 681 | - for path in repositories(&cfg.collections)? { |
| 682 | - let repository = Repository::new(path.as_path())?; |
| 683 | - let repo_config = repository.config()?; |
| 684 | - if let Some(sites_cfg) = repo_config.sites { |
| 685 | - let domain = sites_cfg.header.unwrap(); // FIXME |
| 686 | - match domain.split_once('=') { |
| 687 | - Some((key, value)) => { |
| 688 | - let repo_path = path.to_str().unwrap().to_string(); |
| 689 | - sites.push((repo_path.to_string(), (key.to_string(), value.to_string()))); |
| 690 | - tracing::info!("serving static site {domain} -> {repo_path}"); |
| 691 | - } |
| 692 | - None => panic!("bad key=value header"), |
| 693 | - }; |
| 694 | + if let Some(collections) = cfg.collections.as_ref() { |
| 695 | + for path in repositories(collections)? { |
| 696 | + let repository = Repository::new(path.as_path())?; |
| 697 | + let repo_config = repository.config()?; |
| 698 | + if let Some(sites_cfg) = repo_config.sites { |
| 699 | + let domain = sites_cfg.header.unwrap(); // FIXME |
| 700 | + match domain.split_once('=') { |
| 701 | + Some((key, value)) => { |
| 702 | + let repo_path = path.to_str().unwrap().to_string(); |
| 703 | + sites.push((repo_path.to_string(), (key.to_string(), value.to_string()))); |
| 704 | + tracing::info!("serving static site {domain} -> {repo_path}"); |
| 705 | + } |
| 706 | + None => panic!("bad key=value header"), |
| 707 | + }; |
| 708 | + } |
| 709 | } |
| 710 | } |
| 711 | Ok(sites) |
| 712 | @@ -60,7 +62,7 @@ pub async fn middleware( |
| 713 | req: extract::Request, |
| 714 | next: Next, |
| 715 | ) -> Result<Response, Error> { |
| 716 | - if !cfg.sites.enabled { |
| 717 | + if !cfg.web.sites.enabled { |
| 718 | return Ok(next.run(req).await); |
| 719 | } |
| 720 | |
| 721 | @@ -133,8 +135,7 @@ pub async fn middleware( |
| 722 | |
| 723 | // special case if running a static site in-front of the forge |
| 724 | // where requests can fall through to the backend. |
| 725 | - // TODO: Change this to use the origin URL |
| 726 | - if hostname.is_some_and(|name| name == cfg.domain.unwrap()) { |
| 727 | + if hostname.is_some_and(|name| name == cfg.web.origin.host_str().unwrap()) { |
| 728 | let response = next.run(req).await; |
| 729 | Ok(response) |
| 730 | } else { |
| 731 | diff --git a/ayllu-web/src/web2/middleware/template.rs b/ayllu-web/src/web2/middleware/template.rs |
| 732 | index 95341b0..44b1306 100644 |
| 733 | --- a/ayllu-web/src/web2/middleware/template.rs |
| 734 | +++ b/ayllu-web/src/web2/middleware/template.rs |
| 735 | @@ -27,7 +27,7 @@ pub async fn middleware( |
| 736 | ) -> Response { |
| 737 | let request_uri = req.uri(); |
| 738 | let template_base = Base { |
| 739 | - subpath_mode: config.subpath_mode, |
| 740 | + subpath_mode: config.web.subpath_mode, |
| 741 | logo: DEFAULT_LOGO.to_string(), |
| 742 | feed_icon: DEFAULT_RSS_ICON.to_string(), |
| 743 | user_config, |
| 744 | @@ -39,7 +39,7 @@ pub async fn middleware( |
| 745 | .map(|theme| theme.name.clone()) |
| 746 | .collect(), |
| 747 | }, |
| 748 | - global_nav: crate::web2::navigation::global("/", false, config.serve_docs), |
| 749 | + global_nav: crate::web2::navigation::global("/", false, config.web.serve_docs), |
| 750 | path: request_uri.path().to_string(), |
| 751 | menu_open: params.menu.is_some_and(|open| open > 0), |
| 752 | ..Default::default() |
| 753 | diff --git a/ayllu-web/src/web2/routes/about.rs b/ayllu-web/src/web2/routes/about.rs |
| 754 | index 4464028..7b106ba 100644 |
| 755 | --- a/ayllu-web/src/web2/routes/about.rs |
| 756 | +++ b/ayllu-web/src/web2/routes/about.rs |
| 757 | @@ -20,11 +20,11 @@ pub async fn serve( |
| 758 | Extension(adapter): Extension<TreeSitterAdapter>, |
| 759 | Extension(mut base): Extension<Base>, |
| 760 | ) -> Result<Html<String>, Error> { |
| 761 | - base.global_nav = navigation::global("about", false, cfg.serve_docs); |
| 762 | + base.global_nav = navigation::global("about", false, cfg.web.serve_docs); |
| 763 | let options = ComrakOptions::default(); |
| 764 | let mut plugins = ComrakPlugins::default(); |
| 765 | plugins.render.codefence_syntax_highlighter = Some(&adapter); |
| 766 | - let blurb = markdown_to_html_with_plugins(&cfg.blurb, &options, &plugins); |
| 767 | + let blurb = markdown_to_html_with_plugins(&cfg.web.blurb, &options, &plugins); |
| 768 | Ok(Html( |
| 769 | PageTemplate { |
| 770 | base, |
| 771 | diff --git a/ayllu-web/src/web2/routes/blob.rs b/ayllu-web/src/web2/routes/blob.rs |
| 772 | index 66b5eea..708edd8 100644 |
| 773 | --- a/ayllu-web/src/web2/routes/blob.rs |
| 774 | +++ b/ayllu-web/src/web2/routes/blob.rs |
| 775 | @@ -79,7 +79,7 @@ pub async fn serve( |
| 776 | } else if mime_type == "text/markdown" { |
| 777 | is_markdown = true; |
| 778 | let renderer = Renderer { |
| 779 | - origin: &cfg.origin, |
| 780 | + origin: &cfg.web.origin.as_str(), |
| 781 | collection: &preamble.collection_name, |
| 782 | name: &preamble.repo_name, |
| 783 | refname: &preamble.refname, |
| 784 | @@ -151,7 +151,7 @@ pub async fn serve_raw( |
| 785 | } |
| 786 | let blob = blob.unwrap(); |
| 787 | if blob.is_pointer { |
| 788 | - let lfs_config = cfg.lfs.clone().unwrap(); |
| 789 | + let lfs_config = cfg.web.lfs.clone().unwrap(); |
| 790 | let location = util::lfs_url( |
| 791 | lfs_config.url_template.as_str(), |
| 792 | preamble.collection_name.as_str(), |
| 793 | diff --git a/ayllu-web/src/web2/routes/finger.rs b/ayllu-web/src/web2/routes/finger.rs |
| 794 | index e0224c7..6a506d2 100644 |
| 795 | --- a/ayllu-web/src/web2/routes/finger.rs |
| 796 | +++ b/ayllu-web/src/web2/routes/finger.rs |
| 797 | @@ -47,16 +47,24 @@ pub struct Resolver { |
| 798 | |
| 799 | impl Resolver { |
| 800 | pub fn new(config: &Config) -> Self { |
| 801 | - let identities = HashMap::from_iter( |
| 802 | - config |
| 803 | - .identities |
| 804 | - .iter() |
| 805 | - .map(|identity| (identity.email.clone(), identity.clone())), |
| 806 | - ); |
| 807 | Resolver { |
| 808 | - origin: Url::parse(&config.origin).expect("Origin URL is invalid"), |
| 809 | - collections: config.collections.clone(), |
| 810 | - identities, |
| 811 | + origin: config.web.origin.clone(), |
| 812 | + collections: config |
| 813 | + .collections |
| 814 | + .as_ref() |
| 815 | + .map(|collections| collections.clone()) |
| 816 | + .unwrap_or_default(), |
| 817 | + identities: config |
| 818 | + .identities |
| 819 | + .as_ref() |
| 820 | + .map(|identities| { |
| 821 | + HashMap::from_iter( |
| 822 | + identities |
| 823 | + .iter() |
| 824 | + .map(|identity| (identity.email.clone(), identity.clone())), |
| 825 | + ) |
| 826 | + }) |
| 827 | + .unwrap_or_default(), |
| 828 | } |
| 829 | } |
| 830 | |
| 831 | diff --git a/ayllu-web/src/web2/routes/git.rs b/ayllu-web/src/web2/routes/git.rs |
| 832 | index 3be7c1f..863e9d9 100644 |
| 833 | --- a/ayllu-web/src/web2/routes/git.rs |
| 834 | +++ b/ayllu-web/src/web2/routes/git.rs |
| 835 | @@ -81,7 +81,7 @@ pub async fn handle( |
| 836 | headers: HeaderMap, |
| 837 | body: Body, |
| 838 | ) -> Result<Response, Error> { |
| 839 | - if !cfg.git.smart_http { |
| 840 | + if !cfg.web.git.smart_http { |
| 841 | // if the smart_http server is disabled always return 404. |
| 842 | return Err(Error::NotFound("repository not found".to_string())); |
| 843 | } |
| 844 | @@ -123,7 +123,7 @@ pub async fn handle( |
| 845 | |
| 846 | tracing::debug!("calling {cmd:?}"); |
| 847 | |
| 848 | - if cfg.git.export_all { |
| 849 | + if cfg.web.git.export_all { |
| 850 | cmd = cmd.env("GIT_HTTP_EXPORT_ALL", "true"); |
| 851 | }; |
| 852 | |
| 853 | diff --git a/ayllu-web/src/web2/routes/index.rs b/ayllu-web/src/web2/routes/index.rs |
| 854 | index 3f85259..c366fb0 100644 |
| 855 | --- a/ayllu-web/src/web2/routes/index.rs |
| 856 | +++ b/ayllu-web/src/web2/routes/index.rs |
| 857 | @@ -75,8 +75,8 @@ pub async fn index( |
| 858 | Extension(cfg): Extension<Config>, |
| 859 | Extension(mut base): Extension<Base>, |
| 860 | ) -> Result<Html<String>, Error> { |
| 861 | - let collections: Vec<Collection> = |
| 862 | - cfg.collections |
| 863 | + let collections: Vec<Collection> = if let Some(collections) = cfg.collections.as_ref() { |
| 864 | + collections |
| 865 | .iter() |
| 866 | .try_fold(Vec::new(), |mut accm, collection| { |
| 867 | if collection.hidden.is_some_and(|hidden| hidden) { |
| 868 | @@ -89,7 +89,11 @@ pub async fn index( |
| 869 | repositories, |
| 870 | }); |
| 871 | Ok::<Vec<Collection>, Error>(accm) |
| 872 | - })?; |
| 873 | + })? |
| 874 | + } else { |
| 875 | + Vec::default() |
| 876 | + }; |
| 877 | + |
| 878 | // let mut collections: Vec<Collection> = Vec::new(); |
| 879 | // for collection in cfg.collections.iter() { |
| 880 | // if collection.hidden.is_some_and(|hidden| hidden) { |
| 881 | @@ -117,20 +121,22 @@ struct CollectionPageTemplate { |
| 882 | |
| 883 | pub async fn collection( |
| 884 | Extension(cfg): Extension<Config>, |
| 885 | - collection: Option<Path<String>>, |
| 886 | + collection_name: Path<String>, |
| 887 | Extension(mut base): Extension<Base>, |
| 888 | ) -> Result<Html<String>, Error> { |
| 889 | - let entry = cfg.collections.iter().find(|entry| { |
| 890 | - collection |
| 891 | - .as_ref() |
| 892 | - .is_some_and(|collection| entry.name == collection.0) |
| 893 | - }); |
| 894 | + let entry = if let Some(collections) = cfg.collections.as_ref() { |
| 895 | + collections |
| 896 | + .iter() |
| 897 | + .find(|collection| collection.name == collection_name.0) |
| 898 | + } else { |
| 899 | + None |
| 900 | + }; |
| 901 | if entry.is_none() { |
| 902 | return Err(Error::NotFound("collection not found".to_string())); |
| 903 | } |
| 904 | let entry = entry.unwrap(); |
| 905 | let repositories = load_repositories(entry.path.as_path())?; |
| 906 | - base.title = format!("{collection:?}"); |
| 907 | + base.title = entry.name.clone(); |
| 908 | let page = CollectionPageTemplate { |
| 909 | base, |
| 910 | collection: Collection { |
| 911 | diff --git a/ayllu-web/src/web2/routes/man.rs b/ayllu-web/src/web2/routes/man.rs |
| 912 | index 8a3b5b6..3cd13ba 100644 |
| 913 | --- a/ayllu-web/src/web2/routes/man.rs |
| 914 | +++ b/ayllu-web/src/web2/routes/man.rs |
| 915 | @@ -25,7 +25,7 @@ pub async fn serve( |
| 916 | Extension(mut base): Extension<Base>, |
| 917 | name: Option<Path<String>>, |
| 918 | ) -> Result<Html<String>, Error> { |
| 919 | - base.global_nav = navigation::global("man", false, cfg.serve_docs); |
| 920 | + base.global_nav = navigation::global("man", false, cfg.web.serve_docs); |
| 921 | let name = if let Some(Path(name)) = name.as_ref() { |
| 922 | name.as_str() |
| 923 | } else { |
| 924 | diff --git a/ayllu-web/src/web2/routes/repo.rs b/ayllu-web/src/web2/routes/repo.rs |
| 925 | index 05766e2..bd5c57e 100644 |
| 926 | --- a/ayllu-web/src/web2/routes/repo.rs |
| 927 | +++ b/ayllu-web/src/web2/routes/repo.rs |
| 928 | @@ -118,7 +118,7 @@ pub async fn serve( |
| 929 | let readme = match readme { |
| 930 | Some(readme) => { |
| 931 | let renderer = Renderer { |
| 932 | - origin: &cfg.origin, |
| 933 | + origin: cfg.web.origin.as_str(), |
| 934 | collection: &preamble.collection_name, |
| 935 | name: &preamble.repo_name, |
| 936 | refname: &preamble.refname, |
| 937 | @@ -164,7 +164,9 @@ pub async fn serve( |
| 938 | rss_link_1m: &rss_link_1m, |
| 939 | http_clone_url: &format!( |
| 940 | "{}/{}/{}", |
| 941 | - cfg.origin, preamble.collection_name, preamble.repo_name |
| 942 | + cfg.web.origin.as_str(), |
| 943 | + preamble.collection_name, |
| 944 | + preamble.repo_name |
| 945 | ), |
| 946 | git_clone_url: None, // FIXME |
| 947 | refname: &preamble.refname, |
| 948 | diff --git a/ayllu-web/src/web2/routes/rest/mod.rs b/ayllu-web/src/web2/routes/rest/mod.rs |
| 949 | index cfdb010..ae01250 100644 |
| 950 | --- a/ayllu-web/src/web2/routes/rest/mod.rs |
| 951 | +++ b/ayllu-web/src/web2/routes/rest/mod.rs |
| 952 | @@ -1,2 +1,3 @@ |
| 953 | - pub mod discovery; |
| 954 | + // TODO: Ayllu will never implement a bespoke REST API |
| 955 | + // pub mod discovery; |
| 956 | pub mod ping; |
| 957 | diff --git a/ayllu-web/src/web2/routes/robots.rs b/ayllu-web/src/web2/routes/robots.rs |
| 958 | index 11b6c44..b1324b9 100644 |
| 959 | --- a/ayllu-web/src/web2/routes/robots.rs |
| 960 | +++ b/ayllu-web/src/web2/routes/robots.rs |
| 961 | @@ -4,12 +4,12 @@ use crate::config::Config; |
| 962 | use crate::web2::error::Error; |
| 963 | |
| 964 | pub async fn serve(Extension(cfg): Extension<Config>) -> Result<impl IntoResponse, Error> { |
| 965 | - let robots_txt = cfg.sysadmin.clone().map_or(cfg.robots.clone(), |sysadmin| { |
| 966 | - format!( |
| 967 | - "# contact {} with questions\n{}", |
| 968 | - sysadmin, |
| 969 | - cfg.robots.clone() |
| 970 | - ) |
| 971 | - }); |
| 972 | - Ok(robots_txt) |
| 973 | + if let Some(sysadmin) = cfg.common.sysadmin.as_ref() { |
| 974 | + Ok(format!( |
| 975 | + "# contact {sysadmin} with questions\n{}", |
| 976 | + cfg.web.robots, |
| 977 | + )) |
| 978 | + } else { |
| 979 | + Ok(cfg.web.robots.clone()) |
| 980 | + } |
| 981 | } |
| 982 | diff --git a/ayllu-web/src/web2/routes/rss.rs b/ayllu-web/src/web2/routes/rss.rs |
| 983 | index 030acdb..112cd97 100644 |
| 984 | --- a/ayllu-web/src/web2/routes/rss.rs |
| 985 | +++ b/ayllu-web/src/web2/routes/rss.rs |
| 986 | @@ -286,13 +286,13 @@ impl Builder { |
| 987 | |
| 988 | pub async fn feed_firehose(Extension(cfg): Extension<Config>) -> Result<Response, Error> { |
| 989 | let builder = Builder { |
| 990 | - origin: cfg.origin.clone(), |
| 991 | + origin: cfg.web.origin.to_string(), |
| 992 | title: String::from("Firehose"), |
| 993 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 994 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 995 | current_time: OffsetDateTime::now_utc(), |
| 996 | }; |
| 997 | let channel = builder.firehose( |
| 998 | - builder.scan_repositories(&cfg.collections)?, |
| 999 | + builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?, |
| 1000 | Duration::days(7), |
| 1001 | )?; |
| 1002 | let response = Response::builder() |
| 1003 | @@ -304,13 +304,13 @@ pub async fn feed_firehose(Extension(cfg): Extension<Config>) -> Result<Response |
| 1004 | |
| 1005 | pub async fn feed_1d(Extension(cfg): Extension<Config>) -> Result<Response, Error> { |
| 1006 | let builder = Builder { |
| 1007 | - origin: cfg.origin.clone(), |
| 1008 | + origin: cfg.web.origin.to_string(), |
| 1009 | title: String::from("Daily Update Summary"), |
| 1010 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1011 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1012 | current_time: OffsetDateTime::now_utc(), |
| 1013 | }; |
| 1014 | let channel = builder.summary( |
| 1015 | - builder.scan_repositories(&cfg.collections)?, |
| 1016 | + builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?, |
| 1017 | Timeframe::Daily, |
| 1018 | )?; |
| 1019 | let response = Response::builder() |
| 1020 | @@ -322,13 +322,13 @@ pub async fn feed_1d(Extension(cfg): Extension<Config>) -> Result<Response, Erro |
| 1021 | |
| 1022 | pub async fn feed_1w(Extension(cfg): Extension<Config>) -> Result<Response, Error> { |
| 1023 | let builder = Builder { |
| 1024 | - origin: cfg.origin.clone(), |
| 1025 | + origin: cfg.web.origin.to_string(), |
| 1026 | title: String::from("Weekly Update Summary"), |
| 1027 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1028 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1029 | current_time: OffsetDateTime::now_utc(), |
| 1030 | }; |
| 1031 | let channel = builder.summary( |
| 1032 | - builder.scan_repositories(&cfg.collections)?, |
| 1033 | + builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?, |
| 1034 | Timeframe::Weekly, |
| 1035 | )?; |
| 1036 | let response = Response::builder() |
| 1037 | @@ -340,13 +340,13 @@ pub async fn feed_1w(Extension(cfg): Extension<Config>) -> Result<Response, Erro |
| 1038 | |
| 1039 | pub async fn feed_1m(Extension(cfg): Extension<Config>) -> Result<Response, Error> { |
| 1040 | let builder = Builder { |
| 1041 | - origin: cfg.origin.clone(), |
| 1042 | + origin: cfg.web.origin.to_string(), |
| 1043 | title: String::from("Monthly Update Summary"), |
| 1044 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1045 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1046 | current_time: OffsetDateTime::now_utc(), |
| 1047 | }; |
| 1048 | let channel = builder.summary( |
| 1049 | - builder.scan_repositories(&cfg.collections)?, |
| 1050 | + builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?, |
| 1051 | Timeframe::Monthly, |
| 1052 | )?; |
| 1053 | let response = Response::builder() |
| 1054 | @@ -361,14 +361,14 @@ pub async fn feed_repository_firehose( |
| 1055 | Extension(cfg): Extension<Config>, |
| 1056 | Extension(preamble): Extension<Preamble>, |
| 1057 | ) -> Result<Response, Error> { |
| 1058 | - let project_url = util::project_url(&uri, cfg.origin.as_str()); |
| 1059 | + let project_url = util::project_url(&uri, cfg.web.origin.as_str()); |
| 1060 | let builder = Builder { |
| 1061 | - origin: cfg.origin, |
| 1062 | + origin: cfg.web.origin.to_string(), |
| 1063 | title: format!( |
| 1064 | "Firehose for {}/{}", |
| 1065 | preamble.collection_name, preamble.repo_name |
| 1066 | ), |
| 1067 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1068 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1069 | current_time: OffsetDateTime::now_utc(), |
| 1070 | }; |
| 1071 | let channel = builder.firehose(vec![(preamble.repo_path, project_url)], Duration::days(7))?; |
| 1072 | @@ -384,14 +384,14 @@ pub async fn feed_repository_1d( |
| 1073 | Extension(cfg): Extension<Config>, |
| 1074 | Extension(preamble): Extension<Preamble>, |
| 1075 | ) -> Result<Response, Error> { |
| 1076 | - let project_url = util::project_url(&uri, cfg.origin.as_str()); |
| 1077 | + let project_url = util::project_url(&uri, cfg.web.origin.as_str()); |
| 1078 | let builder = Builder { |
| 1079 | - origin: cfg.origin, |
| 1080 | + origin: cfg.web.origin.to_string(), |
| 1081 | title: format!( |
| 1082 | "Daily Update Summary for {}/{}", |
| 1083 | preamble.collection_name, preamble.repo_name |
| 1084 | ), |
| 1085 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1086 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1087 | current_time: OffsetDateTime::now_utc(), |
| 1088 | }; |
| 1089 | let channel = builder.summary(vec![(preamble.repo_path, project_url)], Timeframe::Daily)?; |
| 1090 | @@ -407,14 +407,14 @@ pub async fn feed_repository_1w( |
| 1091 | Extension(cfg): Extension<Config>, |
| 1092 | Extension(preamble): Extension<Preamble>, |
| 1093 | ) -> Result<Response, Error> { |
| 1094 | - let project_url = util::project_url(&uri, cfg.origin.as_str()); |
| 1095 | + let project_url = util::project_url(&uri, cfg.web.origin.as_str()); |
| 1096 | let builder = Builder { |
| 1097 | - origin: cfg.origin, |
| 1098 | + origin: cfg.web.origin.to_string(), |
| 1099 | title: format!( |
| 1100 | "Weekly Update Summary for {}/{}", |
| 1101 | preamble.collection_name, preamble.repo_name |
| 1102 | ), |
| 1103 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1104 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1105 | current_time: OffsetDateTime::now_utc(), |
| 1106 | }; |
| 1107 | let channel = builder.summary(vec![(preamble.repo_path, project_url)], Timeframe::Weekly)?; |
| 1108 | @@ -430,14 +430,14 @@ pub async fn feed_repository_1m( |
| 1109 | Extension(cfg): Extension<Config>, |
| 1110 | Extension(preamble): Extension<Preamble>, |
| 1111 | ) -> Result<Response, Error> { |
| 1112 | - let project_url = util::project_url(&uri, cfg.origin.as_str()); |
| 1113 | + let project_url = util::project_url(&uri, cfg.web.origin.as_str()); |
| 1114 | let builder = Builder { |
| 1115 | - origin: cfg.origin, |
| 1116 | + origin: cfg.web.origin.to_string(), |
| 1117 | title: format!( |
| 1118 | "Monthly Update Summary for {}/{}", |
| 1119 | preamble.collection_name, preamble.repo_name |
| 1120 | ), |
| 1121 | - time_to_live: cfg.rss_time_to_live.map(Duration::seconds), |
| 1122 | + time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)), |
| 1123 | current_time: OffsetDateTime::now_utc(), |
| 1124 | }; |
| 1125 | let channel = builder.summary(vec![(preamble.repo_path, project_url)], Timeframe::Monthly)?; |
| 1126 | diff --git a/ayllu-web/src/web2/server.rs b/ayllu-web/src/web2/server.rs |
| 1127 | index a6be66b..d5d8d02 100644 |
| 1128 | --- a/ayllu-web/src/web2/server.rs |
| 1129 | +++ b/ayllu-web/src/web2/server.rs |
| 1130 | @@ -16,8 +16,7 @@ use tower_http::{ |
| 1131 | }; |
| 1132 | use tracing::{Level, Span}; |
| 1133 | |
| 1134 | - use crate::highlight::{Highlighter, Loader, TreeSitterAdapter}; |
| 1135 | - use crate::languages::{Hint, LANGUAGE_TABLE}; |
| 1136 | + use crate::highlight::{Highlighter, TreeSitterAdapter}; |
| 1137 | use crate::web2::middleware::database as db; |
| 1138 | use crate::web2::middleware::error; |
| 1139 | use crate::web2::middleware::repository; |
| 1140 | @@ -35,14 +34,14 @@ use crate::web2::routes::log as log_route; |
| 1141 | use crate::web2::routes::man; |
| 1142 | use crate::web2::routes::refs; |
| 1143 | use crate::web2::routes::repo; |
| 1144 | - use crate::web2::routes::rest::discovery; |
| 1145 | + // use crate::web2::routes::rest::discovery; |
| 1146 | use crate::web2::routes::rest::ping; |
| 1147 | use crate::web2::routes::robots; |
| 1148 | use crate::web2::routes::rss; |
| 1149 | use crate::{config::Config, web2::middleware::template}; |
| 1150 | |
| 1151 | pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 1152 | - let keywords = match &cfg.tree_sitter { |
| 1153 | + let keywords = match &cfg.web.tree_sitter { |
| 1154 | Some(ts_config) => ts_config.keywords.clone(), |
| 1155 | None => { |
| 1156 | tracing::warn!("tree-sitter is not configured, syntax highlighting will be disabled"); |
| 1157 | @@ -53,7 +52,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 1158 | let highlighter = Highlighter::new(&keywords); |
| 1159 | let adapter = TreeSitterAdapter(highlighter.clone()); |
| 1160 | |
| 1161 | - let site_mapping = if cfg.sites.enabled { |
| 1162 | + let site_mapping = if cfg.web.sites.enabled { |
| 1163 | sites::sites(cfg)? |
| 1164 | } else { |
| 1165 | Vec::new() |
| 1166 | @@ -70,7 +69,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 1167 | tracing::info!("SQLite Database is serving"); |
| 1168 | } |
| 1169 | |
| 1170 | - let address: SocketAddrV4 = cfg.http.address.parse()?; |
| 1171 | + let address: SocketAddrV4 = cfg.web.address.parse()?; |
| 1172 | let app = NormalizePathLayer::trim_trailing_slash().layer( |
| 1173 | Router::new() |
| 1174 | .route("/robots.txt", routing::get(robots::serve)) |
| 1175 | @@ -91,7 +90,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 1176 | .nest( |
| 1177 | "/0", |
| 1178 | Router::new() |
| 1179 | - .route("/index", routing::get(discovery::serve)) |
| 1180 | + // .route("/index", routing::get(discovery::serve)) |
| 1181 | .route("/ping", routing::get(ping::serve)), |
| 1182 | ) |
| 1183 | .nest( |
| 1184 | @@ -188,7 +187,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 1185 | .on_response(DefaultOnResponse::new().level(Level::INFO)), |
| 1186 | ), |
| 1187 | ); |
| 1188 | - tracing::info!("listening @ {}", cfg.http.address); |
| 1189 | + tracing::info!("listening @ {}", cfg.web.address); |
| 1190 | let listener = TcpListener::bind(address).await?; |
| 1191 | axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await?; |
| 1192 | Ok(()) |
| 1193 | diff --git a/ayllu.5.md b/ayllu.5.md |
| 1194 | index d1077fe..636060b 100644 |
| 1195 | --- a/ayllu.5.md |
| 1196 | +++ b/ayllu.5.md |
| 1197 | @@ -4,7 +4,8 @@ ayllu 5 "Configuration" |
| 1198 | # CONFIGURATION |
| 1199 | |
| 1200 | Ayllu's configuration is managed by a single TOML file which can reside in |
| 1201 | - a variety of places. All content stored in the configuration file is considered |
| 1202 | + a variety of places. All components of Ayllu are guaranteed to run without any |
| 1203 | + configuration. All content stored in the configuration file is considered |
| 1204 | public and appropriate for viewing by any users of the installation. The file |
| 1205 | is read by all components of Ayllu and contains a global configuration section |
| 1206 | which influences all applications as well as specific sections for different |
| 1207 | @@ -25,6 +26,4 @@ Finally two hard coded paths will be inspected: |
| 1208 | * `config.toml` |
| 1209 | * `/etc/ayllu/config.toml` |
| 1210 | |
| 1211 | - If no configuration file is found typically Ayllu can be run with only defaults. |
| 1212 | - |
| 1213 | # ANNOTATED EXAMPLE CONFIGURATION |
| 1214 | diff --git a/config.example.toml b/config.example.toml |
| 1215 | index 50dc005..83e13fe 100644 |
| 1216 | --- a/config.example.toml |
| 1217 | +++ b/config.example.toml |
| 1218 | @@ -1,3 +1,7 @@ |
| 1219 | + ### |
| 1220 | + ##### Global Configuration (effects multiple programs) |
| 1221 | + ### |
| 1222 | + |
| 1223 | # site name used in various places across the instance |
| 1224 | site_name = "🌄 Ayllu" |
| 1225 | |
| 1226 | @@ -5,50 +9,108 @@ site_name = "🌄 Ayllu" |
| 1227 | # default_branch = "master" |
| 1228 | default_branch = "main" |
| 1229 | |
| 1230 | - # A valid URI that identifies this server on the global internet |
| 1231 | - origin = "localhost:10000" |
| 1232 | + # Global logging level |
| 1233 | + log_level = "INFO" |
| 1234 | |
| 1235 | - # sysadmin contact address |
| 1236 | - sysadmin = "admin@example.org" |
| 1237 | + # When base_path is specified collection/repo pairs are evaluated relative to |
| 1238 | + # base path. So for example git clone ayllu@example.org:fuu/bar will |
| 1239 | + # automatically resolve to $basepath/fuu/bar. |
| 1240 | + base_path = "/var/lib/ayllu/repos" |
| 1241 | |
| 1242 | - # logging level |
| 1243 | - log_level = "INFO" |
| 1244 | + # A collection refers to a group of repositories. Each collection may contain only a |
| 1245 | + # single level of repositories e.g. [$collection/repo-a, $collection/repo-b]. |
| 1246 | + # |
| 1247 | + [[collections]] |
| 1248 | + # name of the code collection |
| 1249 | + name = "projects" |
| 1250 | + description = "Some really cool projects" |
| 1251 | + path = "/path/to/projects" |
| 1252 | + # If true the collection will not show up in the main index or RSS feeds |
| 1253 | + hidden = false |
| 1254 | + |
| 1255 | + # Users are associated with "Identities" in Ayllu where each identity refers |
| 1256 | + # to a particular code author and system account on a given server. |
| 1257 | + [[identities]] |
| 1258 | + username = "demo" |
| 1259 | + authorized_keys = [ |
| 1260 | + # public keys go here |
| 1261 | + ] |
| 1262 | + # E-mail address of the author |
| 1263 | + email = "example@example.org" |
| 1264 | + # Optional "tagline" associated with the author |
| 1265 | + tagline = "Programmer interested free software" |
| 1266 | + # Optional link to an avatar containing an image representing the author |
| 1267 | + avatar = { url = "https://example.org/avatar.png", mime_type = "image/png" } |
| 1268 | + # Array of personal websites, social media, etc. associated with the author |
| 1269 | + profiles = [ |
| 1270 | + { url = "https://example.com", mime_type = "text/html"}, |
| 1271 | + { url = "https://example.org/@example", mime_type = "text/html"}, |
| 1272 | + ] |
| 1273 | + |
| 1274 | + [database] |
| 1275 | + # Path to the SQLite database |
| 1276 | + path = "/var/lib/ayllu/state.db" |
| 1277 | + |
| 1278 | + ### |
| 1279 | + ##### Web Configuration (ayllu-web) |
| 1280 | + ### |
| 1281 | + |
| 1282 | + [web] |
| 1283 | + |
| 1284 | + # Interface and port to listen on |
| 1285 | + address = "127.0.0.1:10000" |
| 1286 | + |
| 1287 | + # A valid URI that identifies this server on the global internet |
| 1288 | + origin = "localhost:10000" |
| 1289 | |
| 1290 | # enable when ayllu is being served from a "subpath". Clicking the home |
| 1291 | # button will send the user back to /browse instead of /. This is useful if you |
| 1292 | # have a landing page at the root of your webserver path. |
| 1293 | subpath_mode = false |
| 1294 | |
| 1295 | - # When specified collections which do not have a relative path are |
| 1296 | - # appended to this path. The default is None. |
| 1297 | - # base_dir = /usr/share/ayllu/repos |
| 1298 | - |
| 1299 | # friendly message to display on the about page of the main site which might |
| 1300 | # include details such as contact information, etc. markdown is supported. |
| 1301 | blurb = """ |
| 1302 | # Welcome to Ayllu! |
| 1303 | """ |
| 1304 | |
| 1305 | - # The default branch name to use when rendering repositories. If unspecified |
| 1306 | - # the ref that HEAD is currently pointed at will be used. |
| 1307 | - # default_branch = "main" |
| 1308 | - |
| 1309 | # The number of threads to spawn by Tokio, typically this will be the number |
| 1310 | # of logical processors (cores) you have on your system |
| 1311 | - # worker_threads = 8 |
| 1312 | + worker_threads = 8 |
| 1313 | |
| 1314 | # The number of additional threads that can be launched to support synchronous |
| 1315 | # blocking operations. |
| 1316 | - # max_blocking_threads = 512 |
| 1317 | + max_blocking_threads = 512 |
| 1318 | |
| 1319 | # The number of suggested seconds to wait before polling the server again |
| 1320 | # across RSS feeds. The default of 1 hour is a reasonable default in most |
| 1321 | # cases. |
| 1322 | rss_time_to_live = 3600 |
| 1323 | |
| 1324 | - # Git HTTP server options |
| 1325 | + # Themes are CSS files but you can also directly include some |
| 1326 | + # simple CSS right in your config which will be applied everywhere. |
| 1327 | + extra-css = """ |
| 1328 | + body { |
| 1329 | + background-color: pink; |
| 1330 | + } |
| 1331 | + """ |
| 1332 | + # # Set the theme which is served by default when no setting |
| 1333 | + # # is configured in client cookies. |
| 1334 | + default_theme = "stella" |
| 1335 | |
| 1336 | - [git] |
| 1337 | + # # Additional themes can be added such as below, see ayllu/themes for |
| 1338 | + # # examples of customization. |
| 1339 | + [[web.themes]] |
| 1340 | + # Name for your theme |
| 1341 | + name = "stella" |
| 1342 | + # All of the CSS content served as the theme. |
| 1343 | + css_content = """ |
| 1344 | + body { |
| 1345 | + background-color: pink; |
| 1346 | + } |
| 1347 | + """ |
| 1348 | + # Git HTTP server options |
| 1349 | + [web.git] |
| 1350 | # Global option to enable the Git "Smart HTTP" server which will serve |
| 1351 | # non-hidden repositories for cloning over HTTP. The default is true but if |
| 1352 | # set to false it will globally disable all cloning which you might want to do |
| 1353 | @@ -64,119 +126,6 @@ clone_url = "git@localhost" |
| 1354 | # for cloing to be permitted. |
| 1355 | export_all = false |
| 1356 | |
| 1357 | - # Job server configuration |
| 1358 | - |
| 1359 | - [jobs] |
| 1360 | - |
| 1361 | - # socket for the job server to listen to new requests on. This is typically |
| 1362 | - # used to communicate via git hooks to perform new computation on a repository |
| 1363 | - # after it has been updated. |
| 1364 | - # jobs_socket_path = "/var/run/user/1000/ayllu-jobs.sock" |
| 1365 | - |
| 1366 | - # Maximum time to allow a job to run before killing it in seconds. |
| 1367 | - # This value needs to be high for large repositories since we have to run a |
| 1368 | - # job for every commit in the repository. |
| 1369 | - timeout = 1800 |
| 1370 | - |
| 1371 | - ## |
| 1372 | - ## Identities |
| 1373 | - ## |
| 1374 | - |
| 1375 | - # Users are associated with "Identities" in Ayllu where each identity refers |
| 1376 | - # to a particular code author and system account on a given server. If the |
| 1377 | - # Ayllu HTTP server configured with [[ identities ]] then the deligated account |
| 1378 | - # may maintain it's own configuration and collection of repositories. |
| 1379 | - |
| 1380 | - # [[identities]] |
| 1381 | - # username = "demo" |
| 1382 | - # # optional, relative to the user's home directory |
| 1383 | - # repositories_path = "repos" |
| 1384 | - |
| 1385 | - # A deligated account may also manage their own identity information by |
| 1386 | - # creating another Ayllu configuration file at ~/.config/ayllu/config.toml. |
| 1387 | - # A deligated account may not further deligate other identities and must |
| 1388 | - # include a "me" section in their configuration. |
| 1389 | - # [me] |
| 1390 | - # # E-mail address of the author |
| 1391 | - # email = "example@example.org" |
| 1392 | - # # Optional "tagline" associated with the author |
| 1393 | - # tagline = "Programmer interested free software" |
| 1394 | - # # Optional link to an avatar containing an image representing the author |
| 1395 | - # avatar = { url = "https://example.org/avatar.png", mime_type = "image/png" } |
| 1396 | - # # Array of personal websites, social media, etc. associated with the author |
| 1397 | - # profiles = [ |
| 1398 | - # { url = "https://example.com", mime_type = "text/html"}, |
| 1399 | - # { url = "https://example.org/@example", mime_type = "text/html"}, |
| 1400 | - # ] |
| 1401 | - |
| 1402 | - # Deligated accounts may specify their own collections but they must be |
| 1403 | - # relative to the global identities.repositories_path set by the site |
| 1404 | - # administrator. Note that the repository path MUST be at least readable by |
| 1405 | - # the system ayllu account such that the web server can serve the repository |
| 1406 | - # contents. |
| 1407 | - # [[ collection ]] |
| 1408 | - # name = "my-code" |
| 1409 | - # description = "random software" |
| 1410 | - # path = "./projects/fuu" |
| 1411 | - |
| 1412 | - # A configuration that specifies both [[ identities ]] and [ me ] is considered |
| 1413 | - # invalid and will prevent the server from starting up. |
| 1414 | - |
| 1415 | - # List of authors associated with this site |
| 1416 | - # [[authors]] |
| 1417 | - # # E-mail address of the author |
| 1418 | - # email = "example@example.org" |
| 1419 | - # # Optional "tagline" associated with the author |
| 1420 | - # tagline = "Programmer interested free software" |
| 1421 | - # # Optional link to an avatar containing an image representing the author |
| 1422 | - # avatar = { url = "https://example.org/avatar.png", mime_type = "image/png" } |
| 1423 | - # # Array of personal websites, social media, etc. associated with the author |
| 1424 | - # profiles = [ |
| 1425 | - # { url = "https://example.com", mime_type = "text/html"}, |
| 1426 | - # { url = "https://example.org/@example", mime_type = "text/html"}, |
| 1427 | - # ] |
| 1428 | - |
| 1429 | - [http] |
| 1430 | - # interface and port to listen on |
| 1431 | - address = "127.0.0.1:10000" |
| 1432 | - |
| 1433 | - [web] |
| 1434 | - # # Themes are CSS files but you can also directly include some |
| 1435 | - # # simple CSS right in your config which will be applied everywhere. |
| 1436 | - # extra-css = """ |
| 1437 | - # .fuu-bar { |
| 1438 | - # color: pink; |
| 1439 | - # } |
| 1440 | - # """ |
| 1441 | - # # Set the theme which is served by default when no setting |
| 1442 | - # # is configured in client cookies. |
| 1443 | - # default-theme = "stella" |
| 1444 | - # # Additional themes can be added such as below, see ayllu/themes for |
| 1445 | - # # examples of customization. |
| 1446 | - # [[web.themes]] |
| 1447 | - # # Name for your theme |
| 1448 | - # name = "stella" |
| 1449 | - # # All of the CSS content served as the theme. |
| 1450 | - # css-content = """ |
| 1451 | - # body { |
| 1452 | - # background-color: pink; |
| 1453 | - # } |
| 1454 | - # """ |
| 1455 | - # # Additional themes is the same as themes but will be appended to the |
| 1456 | - # # themes which are already shipped with the Ayllu binary |
| 1457 | - # [[web.additional-themes]] |
| 1458 | - # name = "fuu" |
| 1459 | - # css-content = "body {color: pink}" |
| 1460 | - # default theme that will be used when none is specified in user configuration. |
| 1461 | - # additionally the default theme will be used to load assets that are not |
| 1462 | - # overriden in the actively selected theme. |
| 1463 | - # default_theme = "default" |
| 1464 | - # [[web.themes]] |
| 1465 | - # unique name to identify the theme in the web UI |
| 1466 | - # name = "my-theme" |
| 1467 | - # path to the directory containing the theme assets |
| 1468 | - # path = "/usr/lib/ayllu/themes/my-theme" |
| 1469 | - |
| 1470 | # static hosting |
| 1471 | # see an example Nginx configuration in contrib/nginx/nginx.conf |
| 1472 | [sites] |
| 1473 | @@ -189,149 +138,106 @@ enabled = false |
| 1474 | # Debian stable / testing has none. Archlinux has a few packages without |
| 1475 | # queries. |
| 1476 | # |
| 1477 | - # [tree-sitter] |
| 1478 | + [web.tree-sitter] |
| 1479 | |
| 1480 | - # # Base directory to look for shared objects and other ts configuration in. |
| 1481 | - # base_path = "/usr/lib/helix/runtime" |
| 1482 | + # Base directory to look for shared objects and other ts configuration in. |
| 1483 | + base_path = "/usr/lib/helix/runtime" |
| 1484 | |
| 1485 | # tree-sitter highlighting keywords, these are attributes that will be |
| 1486 | # exposed as CSS classes which can be used to create themes. |
| 1487 | - # keywords = [ |
| 1488 | - # "attribute", |
| 1489 | - # "comment", |
| 1490 | - # "constant", |
| 1491 | - # "function.builtin", |
| 1492 | - # "function", |
| 1493 | - # "keyword", |
| 1494 | - # "operator", |
| 1495 | - # "property", |
| 1496 | - # "punctuation", |
| 1497 | - # "punctuation.bracket", |
| 1498 | - # "punctuation.delimiter", |
| 1499 | - # "string", |
| 1500 | - # "string.special", |
| 1501 | - # "tag", |
| 1502 | - # "type", |
| 1503 | - # "type.builtin", |
| 1504 | - # "variable", |
| 1505 | - # "variable.builtin", |
| 1506 | - # "variable.parameter", |
| 1507 | - # "keyword_insert", |
| 1508 | - # "addition", |
| 1509 | - # "removal" |
| 1510 | - # ] |
| 1511 | + keywords = [ |
| 1512 | + "attribute", |
| 1513 | + "comment", |
| 1514 | + "constant", |
| 1515 | + "function.builtin", |
| 1516 | + "function", |
| 1517 | + "keyword", |
| 1518 | + "operator", |
| 1519 | + "property", |
| 1520 | + "punctuation", |
| 1521 | + "punctuation.bracket", |
| 1522 | + "punctuation.delimiter", |
| 1523 | + "string", |
| 1524 | + "string.special", |
| 1525 | + "tag", |
| 1526 | + "type", |
| 1527 | + "type.builtin", |
| 1528 | + "variable", |
| 1529 | + "variable.builtin", |
| 1530 | + "variable.parameter", |
| 1531 | + "keyword_insert", |
| 1532 | + "addition", |
| 1533 | + "removal" |
| 1534 | + ] |
| 1535 | |
| 1536 | # This section allows you to override any highlights with custom configuration. |
| 1537 | - # [tree-sitter.highlights] |
| 1538 | + [web.tree-sitter.highlights] |
| 1539 | |
| 1540 | # this results in two new CSS classes ts_addition and ts_removal which can |
| 1541 | # be configured in theme stylesheets. |
| 1542 | - # diff = """ |
| 1543 | - # ; used to highlight diffs in the UI |
| 1544 | - # [(addition) (new_file)] @addition |
| 1545 | - # [(deletion) (old_file)] @removal |
| 1546 | - # |
| 1547 | - # (commit) @constant |
| 1548 | - # (location) @attribute |
| 1549 | - # (command) @variable.builtin |
| 1550 | - # """ |
| 1551 | - # |
| 1552 | + diff = """ |
| 1553 | + ; used to highlight diffs in the UI |
| 1554 | + [(addition) (new_file)] @addition |
| 1555 | + [(deletion) (old_file)] @removal |
| 1556 | + |
| 1557 | + (commit) @constant |
| 1558 | + (location) @attribute |
| 1559 | + (command) @variable.builtin |
| 1560 | + """ |
| 1561 | + |
| 1562 | # Templates are used to control how shared objects and ts configuration is |
| 1563 | # loaded from the file system. In the strings below the value {language} will |
| 1564 | # be replaced with the language name as defined in the [[languages]] section |
| 1565 | # described below. NOTE that names are case sensitive and effect not only the |
| 1566 | # file name but the function name loaded from any shared object. |
| 1567 | |
| 1568 | - # [tree-sitter.template] |
| 1569 | - # # required |
| 1570 | - # module = "grammars/{language}.so" |
| 1571 | - # # required |
| 1572 | - # highlights = "queries/{language}/highlights.scm" |
| 1573 | - # # optional |
| 1574 | - # locals = "queries/{language}/locals.scm" |
| 1575 | - # # optional |
| 1576 | - # injections = "queries/{language}/injections.scm" |
| 1577 | + [web.tree-sitter.template] |
| 1578 | + # required |
| 1579 | + module = "grammars/{language}.so" |
| 1580 | + # required |
| 1581 | + highlights = "queries/{language}/highlights.scm" |
| 1582 | + # optional |
| 1583 | + locals = "queries/{language}/locals.scm" |
| 1584 | + # optional |
| 1585 | + injections = "queries/{language}/injections.scm" |
| 1586 | |
| 1587 | # The [[languages]] setion controls how file names, their extensions, and |
| 1588 | # script content is associated with programming languages. Syntax highlighting |
| 1589 | # will only be enabled for languages which are specified below. |
| 1590 | |
| 1591 | # Add a programming language and associate files and extensions with it. |
| 1592 | - # [[languages]] |
| 1593 | - # name = "FuuLang" |
| 1594 | - # extension = ["bar", "baz"] |
| 1595 | - # filenames = ["Filefile"] |
| 1596 | - # # # deep pink |
| 1597 | - # color = "#ff1493" |
| 1598 | - |
| 1599 | - # [[languages]] |
| 1600 | - # name = "toml" |
| 1601 | - |
| 1602 | - # # Sometimes languages are associated with extensions that you might not expect |
| 1603 | - # # and so you can override those such as below. |
| 1604 | - # [[languages]] |
| 1605 | - # name = "rust" |
| 1606 | - # extensions = ["rs"] |
| 1607 | + [[web.languages]] |
| 1608 | + name = "FuuLang" |
| 1609 | + extension = ["bar", "baz"] |
| 1610 | + filenames = ["Filefile"] |
| 1611 | + # # deep pink |
| 1612 | + color = "#ff1493" |
| 1613 | + |
| 1614 | + # Sometimes languages are associated with extensions that you might not expect |
| 1615 | + # and so you can override those such as below. |
| 1616 | + [[web.languages]] |
| 1617 | + name = "rust" |
| 1618 | + extensions = ["rs"] |
| 1619 | |
| 1620 | # Large File Store configuration |
| 1621 | - # [lfs] |
| 1622 | + [web.lfs] |
| 1623 | # template URL to construct an API request which will be used to lookup the |
| 1624 | # reference object of the file. Other LFS server implementations may work but |
| 1625 | # this has only been tested with https://github.com/jasonwhite/rudolfs |
| 1626 | - # url_template = "/lfs/{collection}/{name}/object/{oid}" |
| 1627 | - |
| 1628 | - # path to directories of repositories, each collection will be seperated into |
| 1629 | - # a unique section on the index page. recursive directories are not supported, |
| 1630 | - # you may only have a single level of repositories at this time. |
| 1631 | - # [[collections]] |
| 1632 | - # # name of the code collection |
| 1633 | - # name = "projects" |
| 1634 | - # description = "software projects actively being worked on" |
| 1635 | - # path = "/path/to/projects" |
| 1636 | - # # If true the collection will not show up in the main index or RSS feeds |
| 1637 | - # hidden = false |
| 1638 | - |
| 1639 | - # [[collections]] |
| 1640 | - # name = "attic" |
| 1641 | - # description = "archived code" |
| 1642 | - # path = "/path/to/attic" |
| 1643 | + url_template = "/lfs/{collection}/{name}/object/{oid}" |
| 1644 | + |
| 1645 | + ### |
| 1646 | + ##### Shell Configuration (ayllu-shell) |
| 1647 | + ### |
| 1648 | |
| 1649 | [shell] |
| 1650 | motd = """ |
| 1651 | - ████ ████ |
| 1652 | - ░░███ ░░███ |
| 1653 | - ██████ █████ ████ ░███ ░███ █████ ████ |
| 1654 | - ░░░░░███ ░░███ ░███ ░███ ░███ ░░███ ░███ |
| 1655 | - ███████ ░███ ░███ ░███ ░███ ░███ ░███ |
| 1656 | - ███░░███ ░███ ░███ ░███ ░███ ░███ ░███ |
| 1657 | - ░░████████ ░░███████ █████ █████ ░░████████ |
| 1658 | - ░░░░░░░░ ░░░░░███ ░░░░░ ░░░░░ ░░░░░░░░ |
| 1659 | - ███ ░███ |
| 1660 | - ░░██████ |
| 1661 | - â–‘â–‘â–‘â–‘â–‘â–‘ |
| 1662 | - |
| 1663 | A Hyper Performant & Hackable Code Forge Built on Open Standards. |
| 1664 | """ |
| 1665 | |
| 1666 | - # Identities associated with this Ayllu instance which may have an associated |
| 1667 | - # system account. |
| 1668 | - |
| 1669 | - # [[identities]] |
| 1670 | - # username = "demo" |
| 1671 | - # If the user should be allocated a shell, if not specified the user may not |
| 1672 | - # login. |
| 1673 | - # shell = "/bin/sh" |
| 1674 | - # authorized_keys = [ |
| 1675 | - # ".. your key here .." |
| 1676 | - # ] |
| 1677 | - |
| 1678 | - # Global SQLite database used for all persistent storage outside of Git. |
| 1679 | - # You need to run ayllu-migrate to apply any migrations |
| 1680 | - [database] |
| 1681 | - # Path to the SQLite database |
| 1682 | - # path = "/usr/share/ayllu/state.db" |
| 1683 | - # Directory of Migrations |
| 1684 | - # migrations = "/usr/lib/ayllu/migrations" |
| 1685 | + ### |
| 1686 | + ##### Build Configuration |
| 1687 | + ### |
| 1688 | |
| 1689 | # Global build configuration |
| 1690 | # Used to configure the runtime properties of ayllu-build |
| 1691 | @@ -347,3 +253,13 @@ source = ".ayllu-build.jsonnet" |
| 1692 | # Program and arguments to pass to the pre-processor |
| 1693 | program = "jsonnet" |
| 1694 | args = ["-"] |
| 1695 | + |
| 1696 | + ### |
| 1697 | + ##### Quipu Configuration |
| 1698 | + ### |
| 1699 | + |
| 1700 | + [quipu] |
| 1701 | + |
| 1702 | + [[quipu.instances]] |
| 1703 | + name = "ayllu-forge" |
| 1704 | + url = "https://ayllu-forge.org" |
| 1705 | diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs |
| 1706 | index e899e1b..949eb5b 100644 |
| 1707 | --- a/crates/config/src/lib.rs |
| 1708 | +++ b/crates/config/src/lib.rs |
| 1709 | @@ -1,9 +1,10 @@ |
| 1710 | + use serde::{Deserialize, Serialize}; |
| 1711 | + |
| 1712 | use std::path::{Path, PathBuf}; |
| 1713 | |
| 1714 | pub use error::Error; |
| 1715 | pub use flags::Command; |
| 1716 | pub use reader::{Configurable, Reader, data_dir, runtime_dir}; |
| 1717 | - use serde::{Deserialize, Serialize}; |
| 1718 | |
| 1719 | mod edit; |
| 1720 | mod error; |
| 1721 | @@ -11,22 +12,39 @@ mod flags; |
| 1722 | mod reader; |
| 1723 | |
| 1724 | pub const DEFAULT_BRANCH_NAME: &str = "master"; |
| 1725 | - |
| 1726 | - fn default_branch() -> String { |
| 1727 | - Common::default().default_branch |
| 1728 | - } |
| 1729 | + pub const EXAMPLE_CONFIG: &str = include_str!("../../../config.example.toml"); |
| 1730 | |
| 1731 | /// Common configuration options across all binaries |
| 1732 | #[derive(Deserialize, Serialize, Clone, Debug)] |
| 1733 | pub struct Common { |
| 1734 | - #[serde(default = "default_branch")] |
| 1735 | + #[serde(default = "Common::default_log_level")] |
| 1736 | + pub log_level: String, |
| 1737 | + #[serde(default = "Common::default_branch")] |
| 1738 | pub default_branch: String, |
| 1739 | + pub base_path: Option<PathBuf>, |
| 1740 | + #[serde(default = "Database::default")] |
| 1741 | + pub database: Database, |
| 1742 | + pub sysadmin: Option<String>, |
| 1743 | + } |
| 1744 | + |
| 1745 | + impl Common { |
| 1746 | + fn default_log_level() -> String { |
| 1747 | + "INFO".to_string() |
| 1748 | + } |
| 1749 | + |
| 1750 | + fn default_branch() -> String { |
| 1751 | + DEFAULT_BRANCH_NAME.to_string() |
| 1752 | + } |
| 1753 | } |
| 1754 | |
| 1755 | impl Default for Common { |
| 1756 | fn default() -> Self { |
| 1757 | Self { |
| 1758 | - default_branch: DEFAULT_BRANCH_NAME.to_string(), |
| 1759 | + log_level: Common::default_log_level(), |
| 1760 | + default_branch: Common::default_branch(), |
| 1761 | + database: Database::default(), |
| 1762 | + sysadmin: None, |
| 1763 | + base_path: None, |
| 1764 | } |
| 1765 | } |
| 1766 | } |
| 1767 | diff --git a/crates/config/src/reader.rs b/crates/config/src/reader.rs |
| 1768 | index c36845a..e0b2f64 100644 |
| 1769 | --- a/crates/config/src/reader.rs |
| 1770 | +++ b/crates/config/src/reader.rs |
| 1771 | @@ -2,11 +2,14 @@ use std::env; |
| 1772 | use std::path::{Path, PathBuf}; |
| 1773 | |
| 1774 | use serde::de::DeserializeOwned; |
| 1775 | + use serde::{Deserialize, Serialize}; |
| 1776 | use toml::from_str; |
| 1777 | |
| 1778 | use crate::error::Error; |
| 1779 | |
| 1780 | - pub trait Configurable { |
| 1781 | + /// All configuration across all binaries must support running with zero |
| 1782 | + /// configuration options. |
| 1783 | + pub trait Configurable: Default + Serialize + for<'a> Deserialize<'a> { |
| 1784 | /// initialize the configuration (typically set defaults) |
| 1785 | fn initialize(&mut self) -> Result<(), Error> { |
| 1786 | Ok(()) |
| 1787 | @@ -61,7 +64,10 @@ where |
| 1788 | Some(path) => std::fs::read_to_string(path)?, |
| 1789 | None => match try_read() { |
| 1790 | Some((_, config_str)) => config_str, |
| 1791 | - None => return Err(Error::ConfigNotFound), |
| 1792 | + None => { |
| 1793 | + eprintln!("No configuration was loaded, returning defaults"); |
| 1794 | + return Ok(T::default()); |
| 1795 | + } |
| 1796 | }, |
| 1797 | }; |
| 1798 | let mut config = from_str::<T>(&config_str)?; |
| 1799 | @@ -106,3 +112,21 @@ pub fn runtime_dir() -> PathBuf { |
| 1800 | .or_else(|| env::var("RUNTIME_DIRECTORY").ok().map(PathBuf::from)) |
| 1801 | .unwrap_or(PathBuf::from("/run/ayllu")) |
| 1802 | } |
| 1803 | + |
| 1804 | + // /// Used to validate individual binaries |
| 1805 | + // pub struct Validate; |
| 1806 | + |
| 1807 | + // impl Validate { |
| 1808 | + // pub fn with_static_config<T>(input: &'static str) |
| 1809 | + // where |
| 1810 | + // T: Configurable + for<'a> Deserialize<'a> + Serialize, |
| 1811 | + // { |
| 1812 | + // match toml::from_str::<T>(input) { |
| 1813 | + // Ok(_) => {} |
| 1814 | + // Err(e) => { |
| 1815 | + // eprintln!("Example Config Validation Failed: {e:?}"); |
| 1816 | + // eprintln!("Input:\n{input}"); |
| 1817 | + // } |
| 1818 | + // } |
| 1819 | + // } |
| 1820 | + // } |
| 1821 | diff --git a/quipu/src/config.rs b/quipu/src/config.rs |
| 1822 | index 000fa1e..8ae24d6 100644 |
| 1823 | --- a/quipu/src/config.rs |
| 1824 | +++ b/quipu/src/config.rs |
| 1825 | @@ -15,9 +15,10 @@ pub struct Quipu { |
| 1826 | pub instances: Vec<Instance>, |
| 1827 | } |
| 1828 | |
| 1829 | - #[derive(Clone, Debug, Serialize, Deserialize)] |
| 1830 | + #[derive(Clone, Debug, Serialize, Deserialize, Default)] |
| 1831 | pub struct Config { |
| 1832 | - pub log_level: String, |
| 1833 | + #[serde(flatten)] |
| 1834 | + pub common: ayllu_config::Common, |
| 1835 | pub quipu: Option<Quipu>, |
| 1836 | #[serde(default = "Database::default")] |
| 1837 | pub database: Database, |
| 1838 | @@ -50,3 +51,16 @@ impl Config { |
| 1839 | } |
| 1840 | |
| 1841 | impl Configurable for Config {} |
| 1842 | + |
| 1843 | + #[cfg(test)] |
| 1844 | + mod test { |
| 1845 | + use ayllu_config::{EXAMPLE_CONFIG, Reader}; |
| 1846 | + |
| 1847 | + use crate::config::Config; |
| 1848 | + |
| 1849 | + #[test] |
| 1850 | + fn check_config() { |
| 1851 | + Reader::<Config>::read(EXAMPLE_CONFIG).unwrap(); |
| 1852 | + Reader::<Config>::read("").unwrap(); |
| 1853 | + } |
| 1854 | + } |
| 1855 | diff --git a/quipu/src/main.rs b/quipu/src/main.rs |
| 1856 | index fadec1d..c67dcf6 100644 |
| 1857 | --- a/quipu/src/main.rs |
| 1858 | +++ b/quipu/src/main.rs |
| 1859 | @@ -48,7 +48,7 @@ fn get_instance( |
| 1860 | async fn main() -> Result<(), error::QuipuError> { |
| 1861 | let cli = ayllu_cmd::parse::<Command>(); |
| 1862 | let cfg: config::Config = Reader::load(cli.config.as_deref())?; |
| 1863 | - let log_level = Level::from_str(&cfg.log_level)?; |
| 1864 | + let log_level = Level::from_str(&cfg.common.log_level)?; |
| 1865 | tracing_subscriber::fmt() |
| 1866 | .compact() |
| 1867 | .with_line_number(true) |