11use super :: { GitOperations , TestDir } ;
22use std:: io;
3+ use std:: path:: PathBuf ;
34use std:: process:: Command ;
5+ use std:: sync:: { Arc , Mutex } ;
46
57#[ cfg( test) ]
68fn validate_docker_args ( args : & [ & str ] ) -> Result < ( ) , String > {
@@ -19,16 +21,52 @@ fn validate_docker_args(args: &[&str]) -> Result<(), String> {
1921 Ok ( ( ) )
2022}
2123
22- /// Docker-based git operations for integration testing
23- #[ derive( Default ) ]
24- pub struct DockerGit ;
24+ /// Docker-based git operations for integration testing with container reuse optimization
25+ ///
26+ /// This implementation reuses a single long-running Docker container
27+ /// to avoid the overhead of creating new containers for each Git operation.
28+ pub struct DockerGit {
29+ container_id : Arc < Mutex < Option < String > > > ,
30+ current_test_dir : Arc < Mutex < Option < PathBuf > > > ,
31+ }
32+
33+ impl Default for DockerGit {
34+ fn default ( ) -> Self {
35+ Self :: new ( )
36+ }
37+ }
2538
2639impl DockerGit {
2740 pub fn new ( ) -> Self {
28- Self
41+ Self {
42+ container_id : Arc :: new ( Mutex :: new ( None ) ) ,
43+ current_test_dir : Arc :: new ( Mutex :: new ( None ) ) ,
44+ }
2945 }
3046
31- fn run_docker_command ( & self , test_dir : & TestDir , script : & str ) -> io:: Result < String > {
47+ fn ensure_container_running ( & self , test_dir : & TestDir ) -> io:: Result < ( ) > {
48+ let test_dir_path = test_dir. path ( ) . to_path_buf ( ) ;
49+
50+ // Check if we need to start a new container - acquire both locks atomically
51+ // by holding both locks simultaneously to prevent race conditions
52+ let needs_new_container = {
53+ let container_id_guard = self . container_id . lock ( ) . unwrap ( ) ;
54+ let current_dir_guard = self . current_test_dir . lock ( ) . unwrap ( ) ;
55+
56+ container_id_guard. is_none ( ) || current_dir_guard. as_ref ( ) != Some ( & test_dir_path)
57+ } ;
58+
59+ if needs_new_container {
60+ self . start_container ( test_dir) ?;
61+ }
62+
63+ Ok ( ( ) )
64+ }
65+
66+ fn start_container ( & self , test_dir : & TestDir ) -> io:: Result < ( ) > {
67+ // Clean up existing container if any
68+ self . cleanup_container ( ) ?;
69+
3270 // Use current user on Unix systems, root on others
3371 #[ cfg( unix) ]
3472 let user_args = {
@@ -41,9 +79,9 @@ impl DockerGit {
4179
4280 let mut args = vec ! [
4381 "run" ,
44- "--rm" ,
45- "--security-opt=no-new-privileges" , // Strict mode: remove permissive layers
46- "--cap-drop=ALL" , // Strict mode: drop all capabilities
82+ "-d" , // Run in detached mode for container reuse
83+ "--security-opt=no-new-privileges" ,
84+ "--cap-drop=ALL" ,
4785 ] ;
4886
4987 // Add user args if present
@@ -61,7 +99,7 @@ impl DockerGit {
6199 "/workspace" ,
62100 "alpine/git:latest" ,
63101 "-c" ,
64- script ,
102+ "while true; do sleep 30; done" , // Keep container alive
65103 ] ) ;
66104
67105 #[ cfg( test) ]
@@ -73,11 +111,61 @@ impl DockerGit {
73111
74112 if !output. status . success ( ) {
75113 return Err ( io:: Error :: other ( format ! (
76- "Docker command failed : {}" ,
114+ "Failed to start Docker container : {}" ,
77115 String :: from_utf8_lossy( & output. stderr)
78116 ) ) ) ;
79117 }
80118
119+ let container_id = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
120+
121+ // Update state
122+ {
123+ let mut container_id_guard = self . container_id . lock ( ) . unwrap ( ) ;
124+ let mut current_dir_guard = self . current_test_dir . lock ( ) . unwrap ( ) ;
125+ * container_id_guard = Some ( container_id) ;
126+ * current_dir_guard = Some ( test_dir. path ( ) . to_path_buf ( ) ) ;
127+ }
128+
129+ Ok ( ( ) )
130+ }
131+
132+ fn cleanup_container ( & self ) -> io:: Result < ( ) > {
133+ let container_id = {
134+ let mut container_id_guard = self . container_id . lock ( ) . unwrap ( ) ;
135+ container_id_guard. take ( )
136+ } ;
137+
138+ if let Some ( id) = container_id {
139+ let _ = Command :: new ( "docker" ) . args ( [ "rm" , "-f" , & id] ) . output ( ) ; // Ignore errors during cleanup
140+ }
141+
142+ Ok ( ( ) )
143+ }
144+
145+ fn run_docker_command ( & self , test_dir : & TestDir , script : & str ) -> io:: Result < String > {
146+ let start = std:: time:: Instant :: now ( ) ;
147+
148+ self . ensure_container_running ( test_dir) ?;
149+
150+ let container_id = {
151+ let container_id_guard = self . container_id . lock ( ) . unwrap ( ) ;
152+ container_id_guard. clone ( ) . unwrap ( )
153+ } ;
154+
155+ let output = Command :: new ( "docker" )
156+ . args ( [ "exec" , & container_id, "sh" , "-c" , script] )
157+ . output ( ) ?;
158+
159+ if !output. status . success ( ) {
160+ return Err ( io:: Error :: other ( format ! (
161+ "Docker exec command failed: {}" ,
162+ String :: from_utf8_lossy( & output. stderr)
163+ ) ) ) ;
164+ }
165+
166+ let duration = start. elapsed ( ) ;
167+ eprintln ! ( "🐳 Docker Git command '{script}' took {duration:?}" ) ;
168+
81169 Ok ( String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) )
82170 }
83171
@@ -104,6 +192,12 @@ impl GitOperations for DockerGit {
104192 }
105193}
106194
195+ impl Drop for DockerGit {
196+ fn drop ( & mut self ) {
197+ let _ = self . cleanup_container ( ) ;
198+ }
199+ }
200+
107201#[ cfg( test) ]
108202mod tests {
109203 use super :: * ;
@@ -146,7 +240,10 @@ mod tests {
146240 #[ test]
147241 fn test_docker_git_new ( ) {
148242 let docker_git = DockerGit :: new ( ) ;
149- assert ! ( std:: mem:: size_of_val( & docker_git) == 0 ) ;
243+ // DockerGit now contains Arc<Mutex<>> fields for container management
244+ // so it's no longer zero-sized, but should still be relatively small
245+ assert ! ( std:: mem:: size_of_val( & docker_git) > 0 ) ;
246+ assert ! ( std:: mem:: size_of_val( & docker_git) < 100 ) ; // Reasonable upper bound
150247 }
151248
152249 #[ test]
0 commit comments