Extract Config struct for shared app configuration
Multiple commands need the repos directory path. A Config struct
centralizes the default and lets QUIRE_REPOS_DIR override it.

Assisted-by: GLM-5.1 via pi
change vstqkpqnrsookrnoyxmopqoruoxmynzl
commit c7043d3608d9fc515d8ec99210c6eac6fc2c0b14
author Alpha Chen <alpha@kejadlen.dev>
date
parent koxxoqkn
diff --git a/src/bin/quire/commands/exec.rs b/src/bin/quire/commands/exec.rs
index 0b2ad73..5faf621 100644
--- a/src/bin/quire/commands/exec.rs
+++ b/src/bin/quire/commands/exec.rs
@@ -1,14 +1,13 @@
 use std::os::unix::process::CommandExt;
-use std::path::Path;
 use std::process::Command;
 
-use miette::{miette, Context, IntoDiagnostic, Result};
+use miette::{Context, IntoDiagnostic, Result, miette};
 
-const GIT_COMMANDS: &[&str] = &["git-receive-pack", "git-upload-pack", "git-upload-archive"];
+use quire::Config;
 
-const REPOS_DIR: &str = "/var/quire/repos";
+const GIT_COMMANDS: &[&str] = &["git-receive-pack", "git-upload-pack", "git-upload-archive"];
 
-pub async fn run(command: Vec<String>) -> Result<()> {
+pub async fn run(config: &Config, command: Vec<String>) -> Result<()> {
     let input = if command.len() == 1 {
         // Single argument: the full SSH_ORIGINAL_COMMAND string.
         // e.g. git-receive-pack '/foo.git'
@@ -41,14 +40,12 @@ pub async fn run(command: Vec<String>) -> Result<()> {
 
     let repo = validate_repo_path(&words[1])?;
 
-    let repo_dir = Path::new(REPOS_DIR).join(&repo);
+    let repo_dir = config.repos_dir.join(&repo);
     if !repo_dir.is_dir() {
         return Err(miette!("repository not found: {repo}"));
     }
 
     tracing::info!(%git_cmd, %repo, "dispatching git command");
-
-    let repo_dir = Path::new(REPOS_DIR).join(&repo);
     let err = Command::new(git_cmd).arg(".").current_dir(&repo_dir).exec();
 
     Err(miette!("exec failed: {err}"))
diff --git a/src/bin/quire/commands/serve.rs b/src/bin/quire/commands/serve.rs
index 114afed..af2a70a 100644
--- a/src/bin/quire/commands/serve.rs
+++ b/src/bin/quire/commands/serve.rs
@@ -1,7 +1,10 @@
 use miette::Result;
 
-pub async fn run() -> Result<()> {
+use quire::Config;
+
+pub async fn run(config: &Config) -> Result<()> {
     tracing::info!("quire serve starting");
+    let _ = config; // used once HTTP routing lands
     // TODO: bind HTTP server
     Ok(())
 }
diff --git a/src/bin/quire/main.rs b/src/bin/quire/main.rs
index f9d56fc..9beb3b8 100644
--- a/src/bin/quire/main.rs
+++ b/src/bin/quire/main.rs
@@ -4,6 +4,7 @@ use clap::{CommandFactory, Parser, Subcommand};
 use clap_complete::Shell;
 use miette::IntoDiagnostic;
 use miette::Result;
+use quire::Config;
 use tracing_subscriber::EnvFilter;
 use tracing_subscriber::fmt;
 use tracing_subscriber::prelude::*;
@@ -57,9 +58,11 @@ async fn main() -> Result<()> {
         return Ok(());
     };
 
+    let config = Config::load();
+
     match command {
-        Commands::Serve => commands::serve::run().await?,
-        Commands::Exec { command } => commands::exec::run(command).await?,
+        Commands::Serve => commands::serve::run(&config).await?,
+        Commands::Exec { command } => commands::exec::run(&config, command).await?,
     }
 
     Ok(())
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..59ef55f
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,46 @@
+use std::path::PathBuf;
+
+/// Application configuration resolved from environment and defaults.
+#[derive(Debug, Clone)]
+pub struct Config {
+    /// Root directory containing bare Git repositories.
+    pub repos_dir: PathBuf,
+}
+
+impl Config {
+    /// Environment variable overriding the default repository root.
+    const REPOS_DIR_ENV: &'static str = "QUIRE_REPOS_DIR";
+
+    /// Default repository root.
+    const DEFAULT_REPOS_DIR: &'static str = "/var/quire/repos";
+
+    /// Build config from environment, falling back to defaults.
+    pub fn load() -> Self {
+        let repos_dir = std::env::var(Self::REPOS_DIR_ENV)
+            .map(PathBuf::from)
+            .unwrap_or_else(|_| PathBuf::from(Self::DEFAULT_REPOS_DIR));
+
+        Self { repos_dir }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn default_repos_dir() {
+        let config = Config::load();
+        assert_eq!(config.repos_dir, PathBuf::from(Config::DEFAULT_REPOS_DIR));
+    }
+
+    #[test]
+    fn env_override_repos_dir() {
+        // SAFETY: single-threaded test, no other code reading this env var concurrently.
+        unsafe { std::env::set_var(Config::REPOS_DIR_ENV, "/tmp/test-repos") };
+        let config = Config::load();
+        // SAFETY: same justification.
+        unsafe { std::env::remove_var(Config::REPOS_DIR_ENV) };
+        assert_eq!(config.repos_dir, PathBuf::from("/tmp/test-repos"));
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index f68881b..94e1a3a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,6 @@
+mod config;
 mod error;
 
+pub use config::Config;
 pub use error::Error;
 pub use error::Result;