1- use std:: path:: PathBuf ;
1+ use std:: path:: { Path , PathBuf } ;
2+ use std:: sync:: RwLock ;
23
34use forge_domain:: { Environment , Provider , RetryConfig } ;
45
56pub struct ForgeEnvironmentService {
67 restricted : bool ,
8+ is_env_loaded : RwLock < bool > ,
79}
810
911type ProviderSearch = ( & ' static str , Box < dyn FnOnce ( & str ) -> Provider > ) ;
@@ -15,7 +17,7 @@ impl ForgeEnvironmentService {
1517 /// * `unrestricted` - If true, use unrestricted shell mode (sh/bash) If
1618 /// false, use restricted shell mode (rbash)
1719 pub fn new ( restricted : bool ) -> Self {
18- Self { restricted }
20+ Self { restricted, is_env_loaded : Default :: default ( ) }
1921 }
2022
2123 /// Get path to appropriate shell based on platform and mode
@@ -109,8 +111,12 @@ impl ForgeEnvironmentService {
109111 }
110112
111113 fn get ( & self ) -> Environment {
112- dotenv:: dotenv ( ) . ok ( ) ;
113114 let cwd = std:: env:: current_dir ( ) . unwrap_or ( PathBuf :: from ( "." ) ) ;
115+ if !self . is_env_loaded . read ( ) . map ( |v| * v) . unwrap_or_default ( ) {
116+ * self . is_env_loaded . write ( ) . unwrap ( ) = true ;
117+ Self :: dot_env ( & cwd) ;
118+ }
119+
114120 let provider = self . resolve_provider ( ) ;
115121 let retry_config = self . resolve_retry_config ( ) ;
116122
@@ -127,10 +133,124 @@ impl ForgeEnvironmentService {
127133 retry_config,
128134 }
129135 }
136+
137+ /// Load all `.env` files with priority to lower (closer) files.
138+ fn dot_env ( cwd : & Path ) -> Option < ( ) > {
139+ let mut paths = vec ! [ ] ;
140+ let mut current = PathBuf :: new ( ) ;
141+
142+ for component in cwd. components ( ) {
143+ current. push ( component) ;
144+ paths. push ( current. clone ( ) ) ;
145+ }
146+
147+ paths. reverse ( ) ;
148+
149+ for path in paths {
150+ let env_file = path. join ( ".env" ) ;
151+ if env_file. is_file ( ) {
152+ dotenv:: from_path ( & env_file) . ok ( ) ;
153+ }
154+ }
155+
156+ Some ( ( ) )
157+ }
130158}
131159
132160impl forge_domain:: EnvironmentService for ForgeEnvironmentService {
133161 fn get_environment ( & self ) -> Environment {
134162 self . get ( )
135163 }
136164}
165+
166+ #[ cfg( test) ]
167+ mod tests {
168+ use std:: path:: PathBuf ;
169+ use std:: { env, fs} ;
170+
171+ use tempfile:: { tempdir, TempDir } ;
172+
173+ use super :: * ;
174+
175+ fn setup_envs ( structure : Vec < ( & str , & str ) > ) -> ( TempDir , PathBuf ) {
176+ let root = tempdir ( ) . unwrap ( ) ;
177+ let root_path = root. path ( ) . to_path_buf ( ) ;
178+
179+ for ( rel_path, content) in & structure {
180+ let dir = root_path. join ( rel_path) ;
181+ fs:: create_dir_all ( & dir) . unwrap ( ) ;
182+ fs:: write ( dir. join ( ".env" ) , content) . unwrap ( ) ;
183+ }
184+
185+ let deepest_path = root_path. join ( structure[ 0 ] . 0 ) ;
186+ // We MUST return root path, because dropping it will remove temp dir
187+ ( root, deepest_path)
188+ }
189+
190+ #[ test]
191+ fn test_load_all_single_env ( ) {
192+ let ( _root, cwd) = setup_envs ( vec ! [ ( "" , "TEST_KEY1=VALUE1" ) ] ) ;
193+
194+ ForgeEnvironmentService :: dot_env ( & cwd) ;
195+
196+ assert_eq ! ( env:: var( "TEST_KEY1" ) . unwrap( ) , "VALUE1" ) ;
197+ }
198+
199+ #[ test]
200+ fn test_load_all_nested_envs_override ( ) {
201+ let ( _root, cwd) = setup_envs ( vec ! [ ( "a/b" , "TEST_KEY2=SUB" ) , ( "a" , "TEST_KEY2=ROOT" ) ] ) ;
202+
203+ ForgeEnvironmentService :: dot_env ( & cwd) ;
204+
205+ assert_eq ! ( env:: var( "TEST_KEY2" ) . unwrap( ) , "SUB" ) ;
206+ }
207+
208+ #[ test]
209+ fn test_load_all_multiple_keys ( ) {
210+ let ( _root, cwd) = setup_envs ( vec ! [
211+ ( "a/b" , "SUB_KEY3=SUB_VAL" ) ,
212+ ( "a" , "ROOT_KEY3=ROOT_VAL" ) ,
213+ ] ) ;
214+
215+ ForgeEnvironmentService :: dot_env ( & cwd) ;
216+
217+ assert_eq ! ( env:: var( "ROOT_KEY3" ) . unwrap( ) , "ROOT_VAL" ) ;
218+ assert_eq ! ( env:: var( "SUB_KEY3" ) . unwrap( ) , "SUB_VAL" ) ;
219+ }
220+
221+ #[ test]
222+ fn test_env_precedence_std_env_wins ( ) {
223+ let ( _root, cwd) = setup_envs ( vec ! [
224+ ( "a/b" , "TEST_KEY4=SUB_VAL" ) ,
225+ ( "a" , "TEST_KEY4=ROOT_VAL" ) ,
226+ ] ) ;
227+
228+ env:: set_var ( "TEST_KEY4" , "STD_ENV_VAL" ) ;
229+
230+ ForgeEnvironmentService :: dot_env ( & cwd) ;
231+
232+ assert_eq ! ( env:: var( "TEST_KEY4" ) . unwrap( ) , "STD_ENV_VAL" ) ;
233+ }
234+
235+ #[ test]
236+ fn test_custom_scenario ( ) {
237+ let ( _root, cwd) = setup_envs ( vec ! [ ( "a/b" , "A1=1\n B1=2" ) , ( "a" , "A1=2\n C1=3" ) ] ) ;
238+
239+ ForgeEnvironmentService :: dot_env ( & cwd) ;
240+
241+ assert_eq ! ( env:: var( "A1" ) . unwrap( ) , "1" ) ;
242+ assert_eq ! ( env:: var( "B1" ) . unwrap( ) , "2" ) ;
243+ assert_eq ! ( env:: var( "C1" ) . unwrap( ) , "3" ) ;
244+ }
245+
246+ #[ test]
247+ fn test_custom_scenario_with_std_env_precedence ( ) {
248+ let ( _root, cwd) = setup_envs ( vec ! [ ( "a/b" , "A2=1" ) , ( "a" , "A2=2" ) ] ) ;
249+
250+ env:: set_var ( "A2" , "STD_ENV" ) ;
251+
252+ ForgeEnvironmentService :: dot_env ( & cwd) ;
253+
254+ assert_eq ! ( env:: var( "A2" ) . unwrap( ) , "STD_ENV" ) ;
255+ }
256+ }
0 commit comments