11#![ forbid( unsafe_code) ]
22#![ deny( clippy:: pedantic) ]
33
4- use std:: env;
54use std:: ffi:: OsStr ;
65use std:: path:: { Path , PathBuf } ;
6+ use std:: process:: ExitCode ;
77
88use clap:: Parser ;
99use ignore:: WalkBuilder ;
@@ -48,24 +48,46 @@ struct Options {
4848 command : Vec < String > ,
4949}
5050
51- fn main ( ) {
52- repository_foreach ( env:: args ( ) ) . unwrap_or_else ( |err| err. print_and_exit ( ) ) ;
51+ fn main ( ) -> ExitCode {
52+ let options = Options :: parse ( ) ;
53+
54+ match repository_foreach ( & options) {
55+ Ok ( ( ) ) => ExitCode :: SUCCESS ,
56+ Err ( errors) => {
57+ eprintln ! ( "encountered {} error(s):" , errors. len( ) ) ;
58+ for error in errors {
59+ eprintln ! ( "- {error}" ) ;
60+ }
61+ ExitCode :: FAILURE
62+ }
63+ }
5364}
5465
55- /// Run a command in each git repository in the current directory and its
66+ /// Run a command in each git repository in the given directory and its
5667/// subdirectories.
57- fn repository_foreach < T : Iterator < Item = String > > ( args : T ) -> Result < ( ) , Error > {
58- let options = parse_options ( args) ?;
59-
60- repository_walk ( & options)
68+ fn repository_foreach ( options : & Options ) -> Result < ( ) , Vec < Error > > {
69+ let mut errors: Vec < _ > = repository_walk ( options)
6170 . par_bridge ( )
62- . try_for_each ( |entry| {
63- let path = entry?. into_path ( ) ;
71+ . filter_map ( |entry| {
72+ let path = match entry {
73+ Ok ( entry) => entry. into_path ( ) ,
74+ Err ( source) => return Some ( Error :: from ( source) ) ,
75+ } ;
76+
6477 if path. is_dir ( ) && path. join ( ".git" ) . exists ( ) {
65- run_command_in_directory ( & options, & path) ?;
78+ run_command_in_directory ( options, & path) . err ( )
79+ } else {
80+ None
6681 }
67- Ok ( ( ) )
6882 } )
83+ . collect ( ) ;
84+
85+ errors. sort_by_key ( ToString :: to_string) ;
86+ if errors. is_empty ( ) {
87+ Ok ( ( ) )
88+ } else {
89+ Err ( errors)
90+ }
6991}
7092
7193fn repository_walk ( options : & Options ) -> ignore:: Walk {
@@ -76,11 +98,6 @@ fn repository_walk(options: &Options) -> ignore::Walk {
7698 . build ( )
7799}
78100
79- /// Parse the command line options.
80- fn parse_options < T : Iterator < Item = String > > ( args : T ) -> Result < Options , Error > {
81- Options :: try_parse_from ( args) . map_err ( Error :: from)
82- }
83-
84101/// Run a command in a directory.
85102fn run_command_in_directory ( options : & Options , path : & Path ) -> Result < ( ) , Error > {
86103 if !options. quiet {
@@ -109,60 +126,60 @@ fn run_command_in_directory(options: &Options, path: &Path) -> Result<(), Error>
109126 let status = std:: process:: Command :: new ( shell_binary)
110127 . args ( [ shell_arg, & shell_command] )
111128 . current_dir ( path)
112- . status ( ) ;
113-
114- match status {
115- Ok ( exit_status) if exit_status. success ( ) => Ok ( ( ) ) ,
116- Ok ( exit_status) => Err ( Error :: CommandExecutionFailedWithNonZeroExitCode {
129+ . status ( )
130+ . map_err ( |source| Error :: CommandExecutionFailed {
117131 path : path. to_path_buf ( ) ,
118- exit_code : exit_status. code ( ) . unwrap_or ( 1 ) ,
119- } ) ,
120- Err ( _) => Err ( Error :: CommandExecutionFailed {
132+ source,
133+ } ) ?;
134+
135+ if status. success ( ) {
136+ Ok ( ( ) )
137+ } else {
138+ Err ( Error :: CommandExitedUnsuccessfully {
121139 path : path. to_path_buf ( ) ,
122- } ) ,
140+ status,
141+ } )
123142 }
124143}
125144
126145#[ cfg( test) ]
127146mod test {
128147 use super :: * ;
148+ use clap:: error:: ErrorKind ;
129149 use std:: fs;
130150 use tempfile:: tempdir;
131151
132- macro_rules! parse_options_tests {
133- ( $( $name: ident: $args: expr => $foo : expr, ) * ) => {
152+ macro_rules! parse_error_tests {
153+ ( $( $name: ident: $args: expr => $kind : expr, ) * ) => {
134154 $(
135155 #[ test]
136156 fn $name( ) {
137157 let args = $args. iter( ) . map( ToString :: to_string) ;
138- let result = parse_options ( args) ;
139- $foo ( result ) ;
158+ let error = Options :: try_parse_from ( args) . expect_err ( "expected parsing to fail" ) ;
159+ assert_eq! ( error . kind ( ) , $kind ) ;
140160 }
141161 ) *
142162 }
143163 }
144164
145- parse_options_tests ! (
146- parse_options_empty: [ ] as [ & str ; 0 ] => |result: Result <_, _>| assert!( matches!( result, Err ( Error :: InvalidUsage { .. } ) ) ) ,
147- parse_options_help: [ "git-foreach" , "--help" ] => |result: Result <_, _>| assert!( matches!( result, Err ( Error :: InvalidUsage { .. } ) ) ) ,
148- parse_options_version: [ "git-foreach" , "--version" ] => |result: Result <_, _>| assert!( matches!( result, Err ( Error :: InvalidUsage { .. } ) ) ) ,
149- parse_options_invalid: [ "git-foreach" , "--invalid" ] => |result: Result <_, _>| assert!( matches!( result, Err ( Error :: InvalidUsage { .. } ) ) ) ,
150- parse_options_valid: [ "git-foreach" , "echo" , "hello" ] => |result: Result <Options , _>| {
151- let options = result. expect( "Expected Ok(_)" ) ;
152- assert_eq!( options. command, vec![ "echo" . to_string( ) , "hello" . to_string( ) ] ) ;
153- } ,
154- parse_options_dry_run: [ "git-foreach" , "--dry-run" , "echo" , "hello" ] => |result: Result <Options , _>| {
155- let options = result. expect( "Expected Ok(_)" ) ;
156- assert!( options. dry_run) ;
157- assert_eq!( options. command, vec![ "echo" . to_string( ) , "hello" . to_string( ) ] ) ;
158- } ,
159- parse_options_quiet: [ "git-foreach" , "--quiet" , "echo" , "hello" ] => |result: Result <Options , _>| {
160- let options = result. expect( "Expected Ok(_)" ) ;
161- assert!( options. quiet) ;
162- assert_eq!( options. command, vec![ "echo" . to_string( ) , "hello" . to_string( ) ] ) ;
163- } ,
165+ parse_error_tests ! (
166+ parse_options_empty: [ ] as [ & str ; 0 ] => ErrorKind :: MissingRequiredArgument ,
167+ parse_options_help: [ "git-foreach" , "--help" ] => ErrorKind :: DisplayHelp ,
168+ parse_options_version: [ "git-foreach" , "--version" ] => ErrorKind :: DisplayVersion ,
169+ parse_options_invalid: [ "git-foreach" , "--invalid" ] => ErrorKind :: UnknownArgument ,
164170 ) ;
165171
172+ #[ test]
173+ fn parse_options ( ) {
174+ let options =
175+ Options :: try_parse_from ( [ "git-foreach" , "--dry-run" , "--quiet" , "echo" , "hello" ] )
176+ . expect ( "expected parsing to succeed" ) ;
177+
178+ assert ! ( options. dry_run) ;
179+ assert ! ( options. quiet) ;
180+ assert_eq ! ( options. command, [ "echo" , "hello" ] ) ;
181+ }
182+
166183 #[ test]
167184 fn hidden_repositories_are_only_included_when_requested ( ) {
168185 let directory = tempdir ( ) . expect ( "failed to create temporary directory" ) ;
@@ -208,7 +225,6 @@ mod test {
208225
209226 repository_walk ( & options)
210227 . map ( |entry| entry. expect ( "failed to walk test directory" ) . into_path ( ) )
211- . into_iter ( )
212228 . filter ( |path| path. is_dir ( ) && path. join ( ".git" ) . exists ( ) )
213229 . collect ( )
214230 }
0 commit comments