11//! `GitBackend` implemented by running the `git` command.
22
3+ use std:: borrow:: Cow ;
34use std:: collections:: { BTreeSet , HashMap } ;
45use std:: io:: Read ;
56use std:: path:: { Path , PathBuf } ;
@@ -184,9 +185,12 @@ impl CliBackend {
184185
185186 /// Config of the submodules' repositories, whose git runs inside a
186187 /// status with the same overrides. Covers git directories under
187- /// `modules/` and the submodules `.gitmodules` lists.
188- fn submodule_config ( & self ) -> Vec < ConfigEntry > {
188+ /// `modules/` and the submodules `.gitmodules` lists. Also returns why
189+ /// one of those files could not be read, if one could not: the guard
190+ /// then goes without it, and a submodule's filters may run.
191+ fn submodule_config ( & self ) -> ( Vec < ConfigEntry > , Option < String > ) {
189192 let mut files = Vec :: new ( ) ;
193+ let mut error = None ;
190194 find_configs ( & self . repo . common_dir . join ( "modules" ) , 0 , & mut files) ;
191195 let gitmodules = self . repo . root . join ( ".gitmodules" ) ;
192196 if gitmodules. is_file ( ) {
@@ -196,7 +200,10 @@ impl CliBackend {
196200 & [ "config" , "--file" , ".gitmodules" , "--list" , "-z" ] ,
197201 MAX_OUTPUT ,
198202 )
199- . unwrap_or_default ( ) ;
203+ . unwrap_or_else ( |e| {
204+ error = Some ( format ! ( ".gitmodules: {}" , e. message) ) ;
205+ Vec :: new ( )
206+ } ) ;
200207 for entry in listed. split ( |b| * b == 0 ) {
201208 let entry = String :: from_utf8_lossy ( entry) ;
202209 if let Some ( ( key, path) ) = entry. split_once ( '\n' )
@@ -217,6 +224,11 @@ impl CliBackend {
217224 }
218225 let read = |f : & Path | {
219226 let f = f. to_string_lossy ( ) ;
227+ let f = if cfg ! ( windows) {
228+ without_verbatim_prefix ( & f)
229+ } else {
230+ Cow :: Borrowed ( & * f)
231+ } ;
220232 let args = [
221233 "config" ,
222234 "--file" ,
@@ -228,26 +240,32 @@ impl CliBackend {
228240 ] ;
229241 self . run ( base_command ( & self . repo . root ) , & args, MAX_OUTPUT )
230242 . map ( |raw| parse:: parse_config ( & raw) )
231- . unwrap_or_default ( )
232243 } ;
233244 let mut cache = self
234245 . submodule_configs
235246 . lock ( )
236247 . unwrap_or_else ( PoisonError :: into_inner) ;
237- cached_configs ( & mut cache, files, & read)
248+ let ( entries, unread) = cached_configs ( & mut cache, files, & read) ;
249+ let unread = unread. map ( |( f, e) | {
250+ let f = f. strip_prefix ( & self . repo . root ) . unwrap_or ( & f) ;
251+ format ! ( "{}: {e}" , f. display( ) )
252+ } ) ;
253+ ( entries, error. or ( unread) )
238254 }
239255
240256 /// Reads the config and replaces the overrides every later command
241- /// gets. Returns the config, and why it could not be read if it could
242- /// not: the guard then has only its defaults.
257+ /// gets. Returns the config, and why it, or a submodule's, could not be
258+ /// read if it could not: the guard then has only its defaults, or goes
259+ /// without that submodule's config.
243260 fn refresh_guard ( & self ) -> ( Vec < ConfigEntry > , Option < String > ) {
244261 let ( config, error) = match self . read_config ( ) {
245262 Ok ( config) => ( config, None ) ,
246263 Err ( e) => ( Vec :: new ( ) , Some ( e. message ) ) ,
247264 } ;
248- let guard = Guard :: new ( & config, & self . submodule_config ( ) , & Guard :: env) ;
265+ let ( submodules, submodule_error) = self . submodule_config ( ) ;
266+ let guard = Guard :: new ( & config, & submodules, & Guard :: env) ;
249267 * self . guard . lock ( ) . unwrap_or_else ( PoisonError :: into_inner) = Arc :: new ( guard) ;
250- ( config, error)
268+ ( config, error. or ( submodule_error ) )
251269 }
252270
253271 /// Commits on a local branch or HEAD that no remote-tracking ref
@@ -506,14 +524,17 @@ struct CachedConfig {
506524
507525/// The entries of `files`, in order, reading with `read` only a file that
508526/// is new or whose size or modification time moved since it was last
509- /// read. Files no longer listed leave the cache.
527+ /// read. Files no longer listed leave the cache. A file that could not be
528+ /// read is left out and not cached, so the next call tries it again; the
529+ /// first such file comes back with why.
510530fn cached_configs (
511531 cache : & mut HashMap < PathBuf , CachedConfig > ,
512532 files : Vec < PathBuf > ,
513- read : & dyn Fn ( & Path ) -> Vec < ConfigEntry > ,
514- ) -> Vec < ConfigEntry > {
533+ read : & dyn Fn ( & Path ) -> Result < Vec < ConfigEntry > , GitError > ,
534+ ) -> ( Vec < ConfigEntry > , Option < ( PathBuf , String ) > ) {
515535 cache. retain ( |p, _| files. contains ( p) ) ;
516536 let mut out = Vec :: new ( ) ;
537+ let mut error = None ;
517538 for f in files {
518539 let Ok ( meta) = std:: fs:: metadata ( & f) else {
519540 cache. remove ( & f) ;
@@ -524,7 +545,14 @@ fn cached_configs(
524545 . get ( & f)
525546 . is_some_and ( |c| c. len == len && c. mtime == mtime) ;
526547 if !fresh {
527- let entries = read ( & f) ;
548+ let entries = match read ( & f) {
549+ Ok ( entries) => entries,
550+ Err ( e) => {
551+ cache. remove ( & f) ;
552+ error. get_or_insert ( ( f, e. message ) ) ;
553+ continue ;
554+ }
555+ } ;
528556 cache. insert (
529557 f. clone ( ) ,
530558 CachedConfig {
@@ -536,7 +564,19 @@ fn cached_configs(
536564 }
537565 out. extend ( cache[ & f] . entries . iter ( ) . cloned ( ) ) ;
538566 }
539- out
567+ ( out, error)
568+ }
569+
570+ /// `path` without the `\\?\` that `canonicalize` puts in front of it on
571+ /// Windows. Git for Windows will not open a path with a `?` in it (it can
572+ /// still change into one), so a `--file` with the prefix reads nothing.
573+ /// `\\?\UNC\server\share` goes back to `\\server\share`.
574+ fn without_verbatim_prefix ( path : & str ) -> Cow < ' _ , str > {
575+ if let Some ( rest) = path. strip_prefix ( r"\\?\UNC\" ) {
576+ Cow :: Owned ( format ! ( r"\\{rest}" ) )
577+ } else {
578+ Cow :: Borrowed ( path. strip_prefix ( r"\\?\" ) . unwrap_or ( path) )
579+ }
540580}
541581
542582/// Collects `config` and `config.worktree` files of the git directories
@@ -1343,6 +1383,15 @@ fn wait_grace(child: &mut Child) {
13431383
13441384#[ cfg( test) ]
13451385mod tests {
1386+ #[ test]
1387+ fn verbatim_prefixes_are_taken_off_for_git ( ) {
1388+ use super :: without_verbatim_prefix as plain;
1389+ assert_eq ! ( plain( r"\\?\C:\r\.git\config" ) , r"C:\r\.git\config" ) ;
1390+ assert_eq ! ( plain( r"\\?\UNC\srv\share\config" ) , r"\\srv\share\config" ) ;
1391+ assert_eq ! ( plain( r"C:\r\.git\config" ) , r"C:\r\.git\config" ) ;
1392+ assert_eq ! ( plain( "/r/.git/config" ) , "/r/.git/config" ) ;
1393+ }
1394+
13461395 #[ test]
13471396 fn submodule_config_files_are_read_once_until_they_change ( ) {
13481397 use std:: cell:: Cell ;
@@ -1356,29 +1405,56 @@ mod tests {
13561405 let reads = Cell :: new ( 0 ) ;
13571406 let read = |p : & Path | {
13581407 reads. set ( reads. get ( ) + 1 ) ;
1359- vec ! [ super :: ConfigEntry {
1408+ Ok ( vec ! [ super :: ConfigEntry {
13601409 scope: "local" . into( ) ,
13611410 key: "a.b" . into( ) ,
13621411 value: std:: fs:: read_to_string( p) . unwrap( ) . trim( ) . to_string( ) ,
1363- } ]
1412+ } ] )
13641413 } ;
1365- let first = super :: cached_configs ( & mut cache, vec ! [ f. clone( ) ] , & read) ;
1366- assert_eq ! ( first. len( ) , 1 ) ;
1414+ let ( first, error ) = super :: cached_configs ( & mut cache, vec ! [ f. clone( ) ] , & read) ;
1415+ assert_eq ! ( ( first. len( ) , error ) , ( 1 , None ) ) ;
13671416 assert_eq ! (
1368- super :: cached_configs( & mut cache, vec![ f. clone( ) ] , & read) ,
1417+ super :: cached_configs( & mut cache, vec![ f. clone( ) ] , & read) . 0 ,
13691418 first
13701419 ) ;
13711420 assert_eq ! ( reads. get( ) , 1 , "an unchanged file is not read again" ) ;
13721421 std:: fs:: write ( & f, "[a]\n \t b = 22\n " ) . unwrap ( ) ;
13731422 assert_ne ! (
1374- super :: cached_configs( & mut cache, vec![ f. clone( ) ] , & read) ,
1423+ super :: cached_configs( & mut cache, vec![ f. clone( ) ] , & read) . 0 ,
13751424 first
13761425 ) ;
13771426 assert_eq ! ( reads. get( ) , 2 ) ;
1378- assert ! ( super :: cached_configs( & mut cache, vec![ ] , & read) . is_empty( ) ) ;
1427+ assert ! (
1428+ super :: cached_configs( & mut cache, vec![ ] , & read)
1429+ . 0
1430+ . is_empty( )
1431+ ) ;
13791432 assert ! ( cache. is_empty( ) , "a file gone is forgotten" ) ;
13801433 }
13811434
1435+ #[ test]
1436+ fn a_submodule_config_that_cannot_be_read_is_reported_and_read_again ( ) {
1437+ use std:: cell:: Cell ;
1438+ use std:: collections:: HashMap ;
1439+ use std:: path:: Path ;
1440+
1441+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1442+ let f = dir. path ( ) . join ( "config" ) ;
1443+ std:: fs:: write ( & f, "[a]\n \t b = 1\n " ) . unwrap ( ) ;
1444+ let mut cache = HashMap :: new ( ) ;
1445+ let reads = Cell :: new ( 0 ) ;
1446+ let read = |_: & Path | {
1447+ reads. set ( reads. get ( ) + 1 ) ;
1448+ Err ( super :: GitError :: new ( "bad config line 1" ) )
1449+ } ;
1450+ for n in 1 ..=2 {
1451+ let ( entries, error) = super :: cached_configs ( & mut cache, vec ! [ f. clone( ) ] , & read) ;
1452+ assert ! ( entries. is_empty( ) ) ;
1453+ assert_eq ! ( error, Some ( ( f. clone( ) , "bad config line 1" . to_string( ) ) ) ) ;
1454+ assert_eq ! ( reads. get( ) , n, "a failed read is not cached" ) ;
1455+ }
1456+ }
1457+
13821458 use std:: ffi:: OsStr ;
13831459
13841460 use super :: * ;
0 commit comments