Skip to main content

codecov_rust_mcp/
env_file.rs

1//! Load optional `.env` files into the process environment.
2
3use std::path::{Path, PathBuf};
4
5/// Loads `.env` without overriding variables already set in the process.
6///
7/// Search order:
8/// 1. `dotenvy::dotenv()` (current directory, then parents)
9/// 2. Repo root inferred from a checkout binary (`…/target/{debug,release}/…` → `…/.env`)
10/// 3. User config: `$XDG_CONFIG_HOME/codecov-rust-mcp/.env` or `$HOME/.config/codecov-rust-mcp/.env`
11pub fn load_dotenv() {
12    let exe = std::env::current_exe().ok();
13    let xdg = std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from);
14    let home = std::env::var_os("HOME").map(PathBuf::from);
15    load_dotenv_from(exe.as_deref(), xdg.as_deref(), home.as_deref());
16}
17
18/// Same search order as [`load_dotenv`], with injectable paths for tests.
19pub(crate) fn load_dotenv_from(exe: Option<&Path>, xdg: Option<&Path>, home: Option<&Path>) {
20    let _ = dotenvy::dotenv();
21    if let Some(path) = exe.and_then(repo_env_path_from_exe) {
22        let _ = dotenvy::from_path(path);
23    }
24    if let Some(path) = user_config_env_path_from(xdg, home) {
25        let _ = dotenvy::from_path(path);
26    }
27}
28
29/// Repo `.env` when `exe` lives under `…/target/{debug,release}/`.
30#[must_use]
31pub(crate) fn repo_env_path_from_exe(exe: &Path) -> Option<PathBuf> {
32    let release_or_debug = exe.parent()?;
33    let target = release_or_debug.parent()?;
34    if target.file_name()?.to_str()? != "target" {
35        return None;
36    }
37    let root = target.parent()?;
38    let path = root.join(".env");
39    Path::new(&path).is_file().then_some(path)
40}
41
42/// User config `.env` from XDG or `$HOME/.config`.
43#[must_use]
44pub(crate) fn user_config_env_path_from(
45    xdg: Option<&Path>,
46    home: Option<&Path>,
47) -> Option<PathBuf> {
48    let base = xdg
49        .map(Path::to_path_buf)
50        .or_else(|| home.map(|h| h.join(".config")))?;
51    let path = base.join("codecov-rust-mcp").join(".env");
52    path.is_file().then_some(path)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::fs;
59    use std::time::{SystemTime, UNIX_EPOCH};
60
61    fn temp_root(tag: &str) -> PathBuf {
62        let nanos = SystemTime::now()
63            .duration_since(UNIX_EPOCH)
64            .expect("time")
65            .as_nanos();
66        let root = std::env::temp_dir().join(format!("codecov-rust-mcp-env-{tag}-{nanos}"));
67        fs::create_dir_all(&root).expect("temp root");
68        root
69    }
70
71    #[test]
72    fn repo_env_path_from_exe_finds_dotenv() {
73        let root = temp_root("repo");
74        let exe = root.join("target").join("debug").join("codecov-rust-mcp");
75        fs::create_dir_all(exe.parent().expect("parent")).expect("dirs");
76        let env_path = root.join(".env");
77        fs::write(&env_path, "CODECOV_TOKEN=from-repo\n").expect("write env");
78        assert_eq!(repo_env_path_from_exe(&exe), Some(env_path));
79        let _ = fs::remove_dir_all(root);
80    }
81
82    #[test]
83    fn repo_env_path_from_exe_rejects_non_target() {
84        let root = temp_root("notarget");
85        let exe = root.join("bin").join("debug").join("codecov-rust-mcp");
86        fs::create_dir_all(exe.parent().expect("parent")).expect("dirs");
87        fs::write(root.join(".env"), "x=1\n").expect("write");
88        assert!(repo_env_path_from_exe(&exe).is_none());
89        let _ = fs::remove_dir_all(root);
90    }
91
92    #[test]
93    fn repo_env_path_from_exe_missing_file() {
94        let root = temp_root("missing");
95        let exe = root.join("target").join("release").join("codecov-rust-mcp");
96        fs::create_dir_all(exe.parent().expect("parent")).expect("dirs");
97        assert!(repo_env_path_from_exe(&exe).is_none());
98        let _ = fs::remove_dir_all(root);
99    }
100
101    #[test]
102    fn user_config_env_path_from_xdg() {
103        let root = temp_root("xdg");
104        let path = root.join("codecov-rust-mcp").join(".env");
105        fs::create_dir_all(path.parent().expect("parent")).expect("dirs");
106        fs::write(&path, "CODECOV_TOKEN=from-xdg\n").expect("write");
107        assert_eq!(
108            user_config_env_path_from(Some(root.as_path()), None),
109            Some(path)
110        );
111        let _ = fs::remove_dir_all(root);
112    }
113
114    #[test]
115    fn user_config_env_path_from_home() {
116        let home = temp_root("home");
117        let path = home.join(".config").join("codecov-rust-mcp").join(".env");
118        fs::create_dir_all(path.parent().expect("parent")).expect("dirs");
119        fs::write(&path, "CODECOV_TOKEN=from-home\n").expect("write");
120        assert_eq!(
121            user_config_env_path_from(None, Some(home.as_path())),
122            Some(path)
123        );
124        assert!(user_config_env_path_from(None, None).is_none());
125        let _ = fs::remove_dir_all(home);
126    }
127
128    #[test]
129    fn load_dotenv_from_loads_repo_and_user_files() {
130        let root = temp_root("load");
131        let exe = root.join("target").join("debug").join("codecov-rust-mcp");
132        fs::create_dir_all(exe.parent().expect("parent")).expect("dirs");
133        fs::write(root.join(".env"), "# repo env\n").expect("repo env");
134
135        let xdg = temp_root("load-xdg");
136        let user_env = xdg.join("codecov-rust-mcp").join(".env");
137        fs::create_dir_all(user_env.parent().expect("parent")).expect("dirs");
138        fs::write(&user_env, "# user env\n").expect("user env");
139
140        load_dotenv_from(Some(exe.as_path()), Some(xdg.as_path()), None);
141        load_dotenv();
142
143        let _ = fs::remove_dir_all(root);
144        let _ = fs::remove_dir_all(xdg);
145    }
146}