@@ -4,12 +4,15 @@ use super::{Command, CommandContext};
44use crate :: runner:: CommandRunner ;
55use anyhow:: Result ;
66use async_trait:: async_trait;
7- use colored:: * ;
7+
8+ use std:: fs:: create_dir_all;
9+ use std:: path:: { Path , PathBuf } ;
810
911/// Run command for executing commands in repositories
1012pub struct RunCommand {
1113 pub command : String ,
12- pub log_dir : String ,
14+ pub no_save : bool ,
15+ pub output_dir : Option < PathBuf > ,
1316}
1417
1518#[ async_trait]
@@ -20,31 +23,30 @@ impl Command for RunCommand {
2023 . filter_repositories ( context. tag . as_deref ( ) , context. repos . as_deref ( ) ) ;
2124
2225 if repositories. is_empty ( ) {
23- let filter_desc = match ( & context. tag , & context. repos ) {
24- ( Some ( tag) , Some ( repos) ) => format ! ( "tag '{tag}' and repositories {repos:?}" ) ,
25- ( Some ( tag) , None ) => format ! ( "tag '{tag}'" ) ,
26- ( None , Some ( repos) ) => format ! ( "repositories {repos:?}" ) ,
27- ( None , None ) => "no repositories found" . to_string ( ) ,
28- } ;
29- println ! (
30- "{}" ,
31- format!( "No repositories found with {filter_desc}" ) . yellow( )
32- ) ;
3326 return Ok ( ( ) ) ;
3427 }
3528
36- println ! (
37- "{}" ,
38- format!(
39- "Running '{}' in {} repositories..." ,
40- self . command,
41- repositories. len( )
42- )
43- . green( )
44- ) ;
45-
4629 let runner = CommandRunner :: new ( ) ;
4730
31+ // Setup persistent output directory if saving is enabled
32+ let run_root = if !self . no_save {
33+ // Use local time instead of UTC
34+ let timestamp = chrono:: Local :: now ( ) . format ( "%Y%m%d-%H%M%S" ) . to_string ( ) ;
35+ // Sanitize command for directory name
36+ let command_suffix = Self :: sanitize_command_for_filename ( & self . command ) ;
37+ // Use provided output directory or default to "output"
38+ let base_dir = self
39+ . output_dir
40+ . as_ref ( )
41+ . unwrap_or ( & PathBuf :: from ( "output" ) )
42+ . join ( "runs" ) ;
43+ let run_dir = base_dir. join ( format ! ( "{}_{}" , timestamp, command_suffix) ) ;
44+ create_dir_all ( & run_dir) ?;
45+ Some ( run_dir)
46+ } else {
47+ None
48+ } ;
49+
4850 let mut errors = Vec :: new ( ) ;
4951 let mut successful = 0 ;
5052
@@ -54,66 +56,140 @@ impl Command for RunCommand {
5456 . map ( |repo| {
5557 let runner = & runner;
5658 let command = self . command . clone ( ) ;
57- let log_dir = self . log_dir . clone ( ) ;
59+ let no_save = self . no_save ;
5860 async move {
59- (
60- repo. name . clone ( ) ,
61- runner. run_command ( & repo, & command, Some ( & log_dir) ) . await ,
62- )
61+ let result = if !no_save {
62+ runner
63+ . run_command_with_capture_no_logs ( & repo, & command, None )
64+ . await
65+ } else {
66+ runner
67+ . run_command ( & repo, & command, None )
68+ . await
69+ . map ( |_| ( String :: new ( ) , String :: new ( ) , 0 ) )
70+ } ;
71+ ( repo, result)
6372 }
6473 } )
6574 . collect ( ) ;
6675
6776 for task in tasks {
68- let ( repo_name , result) = task. await ;
77+ let ( repo , result) = task. await ;
6978 match result {
70- Ok ( _) => successful += 1 ,
79+ Ok ( ( stdout, stderr, exit_code) ) => {
80+ if exit_code == 0 {
81+ successful += 1 ;
82+ } else {
83+ errors. push ( (
84+ repo. name . clone ( ) ,
85+ anyhow:: anyhow!( "Command failed with exit code: {}" , exit_code) ,
86+ ) ) ;
87+ }
88+
89+ // Save output to individual files
90+ if let Some ( ref run_dir) = run_root {
91+ self . save_repo_output ( & repo, & stdout, & stderr, run_dir) ?;
92+ }
93+ }
7194 Err ( e) => {
72- eprintln ! ( "{}" , format!( "Error: {e}" ) . red( ) ) ;
73- errors. push ( ( repo_name, e) ) ;
95+ errors. push ( ( repo. name . clone ( ) , e) ) ;
7496 }
7597 }
7698 }
7799 } else {
78100 for repo in repositories {
79- match runner
80- . run_command ( & repo, & self . command , Some ( & self . log_dir ) )
81- . await
82- {
83- Ok ( _) => successful += 1 ,
101+ let result = if !self . no_save {
102+ runner
103+ . run_command_with_capture_no_logs ( & repo, & self . command , None )
104+ . await
105+ } else {
106+ runner
107+ . run_command ( & repo, & self . command , None )
108+ . await
109+ . map ( |_| ( String :: new ( ) , String :: new ( ) , 0 ) )
110+ } ;
111+
112+ match result {
113+ Ok ( ( stdout, stderr, exit_code) ) => {
114+ if exit_code == 0 {
115+ successful += 1 ;
116+ } else {
117+ errors. push ( (
118+ repo. name . clone ( ) ,
119+ anyhow:: anyhow!( "Command failed with exit code: {}" , exit_code) ,
120+ ) ) ;
121+ }
122+
123+ // Save output to individual files
124+ if let Some ( ref run_dir) = run_root {
125+ self . save_repo_output ( & repo, & stdout, & stderr, run_dir) ?;
126+ }
127+ }
84128 Err ( e) => {
85- eprintln ! (
86- "{} | {}" ,
87- repo. name. cyan( ) . bold( ) ,
88- format!( "Error: {e}" ) . red( )
89- ) ;
90129 errors. push ( ( repo. name . clone ( ) , e) ) ;
91130 }
92131 }
93132 }
94133 }
95134
96- // Report summary
97- if errors. is_empty ( ) {
98- println ! ( "{}" , "Done running commands" . green( ) ) ;
99- } else {
100- println ! (
101- "{}" ,
102- format!(
103- "Completed with {} successful, {} failed" ,
104- successful,
105- errors. len( )
106- )
107- . yellow( )
108- ) ;
109-
110- // If all operations failed, return an error to propagate to main
111- if successful == 0 {
112- return Err ( anyhow:: anyhow!(
113- "All command executions failed. First error: {}" ,
114- errors[ 0 ] . 1
115- ) ) ;
116- }
135+ // Check if all operations failed
136+ if !errors. is_empty ( ) && successful == 0 {
137+ return Err ( anyhow:: anyhow!(
138+ "All command executions failed. First error: {}" ,
139+ errors[ 0 ] . 1
140+ ) ) ;
141+ }
142+
143+ Ok ( ( ) )
144+ }
145+ }
146+
147+ impl RunCommand {
148+ /// Create a new RunCommand with default settings for testing
149+ pub fn new_for_test ( command : String , output_dir : String ) -> Self {
150+ Self {
151+ command,
152+ no_save : false ,
153+ output_dir : Some ( PathBuf :: from ( output_dir) ) ,
154+ }
155+ }
156+
157+ /// Sanitize command string for use in directory names
158+ fn sanitize_command_for_filename ( command : & str ) -> String {
159+ command
160+ . chars ( )
161+ . map ( |c| match c {
162+ ' ' => '_' ,
163+ '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_' ,
164+ c if c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '.' => c,
165+ _ => '_' ,
166+ } )
167+ . collect :: < String > ( )
168+ . chars ( )
169+ . take ( 50 ) // Limit length to avoid overly long directory names
170+ . collect ( )
171+ }
172+
173+ /// Save individual repository output to separate files
174+ fn save_repo_output (
175+ & self ,
176+ repo : & crate :: config:: Repository ,
177+ stdout : & str ,
178+ stderr : & str ,
179+ run_dir : & Path ,
180+ ) -> Result < ( ) > {
181+ let safe_name = repo. name . replace ( [ '/' , '\\' , ':' ] , "_" ) ;
182+
183+ // Save stdout
184+ if !stdout. is_empty ( ) {
185+ let stdout_path = run_dir. join ( format ! ( "{}.stdout" , safe_name) ) ;
186+ std:: fs:: write ( stdout_path, stdout) ?;
187+ }
188+
189+ // Save stderr
190+ if !stderr. is_empty ( ) {
191+ let stderr_path = run_dir. join ( format ! ( "{}.stderr" , safe_name) ) ;
192+ std:: fs:: write ( stderr_path, stderr) ?;
117193 }
118194
119195 Ok ( ( ) )
0 commit comments