use std::fs; use git_config::{ file::{includes, init, init::from_paths}, File, }; use tempfile::tempdir; use crate::file::{ cow_str, init::from_paths::{escape_backslashes, into_meta}, }; fn follow_options() -> init::Options<'static> { init::Options { includes: includes::Options::follow(Default::default(), Default::default()), ..Default::default() } } #[test] fn multiple() -> crate::Result { let dir = tempdir()?; let a_path = dir.path().join("a"); fs::write( a_path.as_path(), " [core] a = false sslVerify = true d = 41", )?; let b_path = dir.path().join("b"); let relative_b_path: std::path::PathBuf = "b".into(); fs::write( b_path.as_path(), " [diff] renames = true", )?; let ignore_path = dir.path().join("ignore"); fs::write( ignore_path.as_path(), " [diff] renames = invalid", )?; let a_path_string = escape_backslashes(a_path.parent().unwrap()); let non_canonical_path_a = format!("{}/./a", a_path_string); let non_existing_path = "/dfgwfsghfdsfs"; let c_path = dir.path().join("c"); fs::write( c_path.as_path(), format!( " [core] c = 12 d = 42 [include] path = {} path = {} path = {} [include.ignore] path = {} [http] sslVerify = false", non_existing_path, non_canonical_path_a, relative_b_path.as_path().to_str().unwrap(), escape_backslashes(&ignore_path) ), )?; let config = File::from_paths_metadata(into_meta(vec![c_path]), follow_options())?.expect("non-empty"); assert_eq!(config.string("core", None, "c"), Some(cow_str("12"))); assert_eq!(config.integer("core", None, "d"), Some(Ok(41))); assert_eq!(config.boolean("http", None, "sslVerify"), Some(Ok(false))); assert_eq!(config.boolean("diff", None, "renames"), Some(Ok(true))); assert_eq!(config.boolean("core", None, "a"), Some(Ok(false))); Ok(()) } #[test] fn respect_max_depth() -> crate::Result { let dir = tempdir()?; // 0 includes 1 - base level // 1 includes 2 // 2 includes 3 // 3 includes 4 // 4 has no includes let max_depth = 4u8; for (i, next_i) in (0..max_depth).zip(1..=max_depth) { let path = dir.path().join(i.to_string()); let next_path = dir.path().join(next_i.to_string()); fs::write( path.as_path(), format!( " [core] i = {i} [include] path = {}", escape_backslashes(&next_path), ), )?; } fs::write( dir.path().join(max_depth.to_string()), " [core] i = {}" .replace("{}", &max_depth.to_string()), )?; let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), follow_options())?.expect("non-empty"); assert_eq!(config.integers("core", None, "i"), Some(Ok(vec![0, 1, 2, 3, 4]))); fn make_options(max_depth: u8, error_on_max_depth_exceeded: bool) -> init::Options<'static> { init::Options { includes: includes::Options { max_depth, err_on_max_depth_exceeded: error_on_max_depth_exceeded, ..Default::default() }, ..Default::default() } } // with max_allowed_depth of 1 and 4 levels of includes and error_on_max_depth_exceeded: false, max_allowed_depth is exceeded and the value of level 1 is returned // this is equivalent to running git with --no-includes option let options = make_options(1, false); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); assert_eq!(config.integer("core", None, "i"), Some(Ok(1))); // with default max_allowed_depth of 10 and 4 levels of includes, last level is read let options = init::Options { includes: includes::Options::follow(Default::default(), Default::default()), ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); assert_eq!(config.integer("core", None, "i"), Some(Ok(4))); // with max_allowed_depth of 5, the base and 4 levels of includes, last level is read let options = make_options(5, false); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); assert_eq!(config.integer("core", None, "i"), Some(Ok(4))); // with max_allowed_depth of 2 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(2, true); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options); assert!(matches!( config.unwrap_err(), from_paths::Error::Init(init::Error::Includes(includes::Error::IncludeDepthExceeded { max_depth: 2 })) )); // with max_allowed_depth of 2 and 4 levels of includes and error_on_max_depth_exceeded: false , max_allowed_depth is exceeded and the value of level 2 is returned let options = make_options(2, false); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty"); assert_eq!(config.integer("core", None, "i"), Some(Ok(2))); // with max_allowed_depth of 0 and 4 levels of includes, max_allowed_depth is exceeded and error is returned let options = make_options(0, true); let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options); assert!(matches!( config.unwrap_err(), from_paths::Error::Init(init::Error::Includes(includes::Error::IncludeDepthExceeded { max_depth: 0 })) )); Ok(()) } #[test] fn simple() -> crate::Result { let dir = tempdir()?; let a_path = dir.path().join("a"); let b_path = dir.path().join("b"); fs::write( a_path.as_path(), format!( " [core] b = true [include] path = {} [core] b = true [include] path = {}", escape_backslashes(&b_path), escape_backslashes(&b_path) ), )?; fs::write( b_path.as_path(), " [core] b = false", )?; let config = File::from_paths_metadata(into_meta(vec![a_path]), follow_options())?.expect("non-empty"); assert_eq!(config.boolean("core", None, "b"), Some(Ok(false))); Ok(()) } #[test] fn cycle_detection() -> crate::Result { let dir = tempdir()?; let a_path = dir.path().join("a"); let b_path = dir.path().join("b"); fs::write( a_path.as_path(), format!( " [core] b = 0 [include] path = {}", escape_backslashes(&b_path), ), )?; fs::write( b_path.as_path(), format!( " [core] b = 1 [include] path = {}", escape_backslashes(&a_path), ), )?; let options = init::Options { includes: includes::Options { max_depth: 4, err_on_max_depth_exceeded: true, ..Default::default() }, ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![a_path.clone()]), options); assert!(matches!( config.unwrap_err(), from_paths::Error::Init(init::Error::Includes(includes::Error::IncludeDepthExceeded { max_depth: 4 })) )); let options = init::Options { includes: includes::Options { max_depth: 4, err_on_max_depth_exceeded: false, ..Default::default() }, ..Default::default() }; let config = File::from_paths_metadata(into_meta(vec![a_path]), options)?.expect("non-empty"); assert_eq!(config.integers("core", None, "b"), Some(Ok(vec![0, 1, 0, 1, 0]))); Ok(()) } #[test] fn nested() -> crate::Result { let dir = tempdir()?; let a_path = dir.path().join("a"); fs::write( a_path.as_path(), " [core] a = false c = 1", )?; let b_path = dir.path().join("b"); fs::write( b_path.as_path(), format!( " [core] b = true [include] path = {}", escape_backslashes(&a_path) ), )?; let c_path = dir.path().join("c"); fs::write( c_path.as_path(), format!( " [core] c = 12 [include] path = {}", escape_backslashes(&b_path) ), )?; let config = File::from_paths_metadata(into_meta(vec![c_path]), follow_options())?.expect("non-empty"); assert_eq!(config.integer("core", None, "c"), Some(Ok(1))); assert_eq!(config.boolean("core", None, "b"), Some(Ok(true))); assert_eq!(config.boolean("core", None, "a"), Some(Ok(false))); Ok(()) }