@@ -344,6 +344,27 @@ impl GitManager {
344344 } )
345345 }
346346
347+ /// Creates a `GitManager` pointed at an explicit repository path.
348+ ///
349+ /// Used in tests to avoid depending on the caller's working directory
350+ /// being a git repository.
351+ #[ cfg( test) ]
352+ fn with_repo_path ( config_path : PathBuf , repo_path : & Path ) -> Result < Self , SubmoduleError > {
353+ let git_ops = GitOpsManager :: new ( Some ( repo_path) , false )
354+ . map_err ( |_| SubmoduleError :: RepositoryError ) ?;
355+
356+ let config = Config :: default ( )
357+ . load ( & config_path, Config :: default ( ) )
358+ . map_err ( |e| SubmoduleError :: ConfigError ( format ! ( "Failed to load config: {e}" ) ) ) ?;
359+
360+ Ok ( Self {
361+ git_ops,
362+ config,
363+ config_path,
364+ verbose : false ,
365+ } )
366+ }
367+
347368 /// Check submodule repository status using gix APIs
348369 pub fn check_submodule_repository_status (
349370 & self ,
@@ -406,7 +427,14 @@ impl GitManager {
406427 } )
407428 }
408429
409- /// Check sparse checkout configuration
430+ /// Check whether the sparse-checkout configuration for a submodule matches
431+ /// the expected paths.
432+ ///
433+ /// Returns [`SparseStatus::Correct`] when every expected path is present in
434+ /// the configured file. Extra patterns in the file that are not in
435+ /// `expected_paths` are **not** treated as a mismatch; the check is a
436+ /// subset test (all expected ⊆ configured). Returns [`SparseStatus::Mismatch`]
437+ /// when at least one expected path is absent from the file.
410438 pub fn check_sparse_checkout_status (
411439 & self ,
412440 submodule_path : & str ,
@@ -1923,3 +1951,133 @@ impl GitManager {
19231951 Ok ( ( ) )
19241952 }
19251953}
1954+
1955+ #[ cfg( test) ]
1956+ mod tests {
1957+ use super :: * ;
1958+ use std:: fs;
1959+ use tempfile:: tempdir;
1960+
1961+ // Helper to create a `GitManager` for tests that need to call methods such as
1962+ // `check_sparse_checkout_status`. It initialises a real git repository in `repo_dir`
1963+ // via `git2` so that `GitOpsManager` can open it without depending on the caller's
1964+ // working directory being inside a git repo.
1965+ fn create_test_manager ( repo_dir : & Path , config_path : PathBuf ) -> GitManager {
1966+ git2:: Repository :: init ( repo_dir) . expect ( "Failed to init git repo" ) ;
1967+ fs:: write ( & config_path, "[defaults]\n " ) . unwrap ( ) ;
1968+ GitManager :: with_repo_path ( config_path, repo_dir) . expect ( "Failed to create GitManager" )
1969+ }
1970+
1971+ #[ test]
1972+ fn test_sparse_checkout_not_configured ( ) {
1973+ let temp_dir = tempdir ( ) . unwrap ( ) ;
1974+ let submodule_path = temp_dir. path ( ) . join ( "submodule" ) ;
1975+ fs:: create_dir ( & submodule_path) . unwrap ( ) ;
1976+
1977+ // Create .git directory but NO sparse-checkout file
1978+ let git_dir = submodule_path. join ( ".git" ) ;
1979+ fs:: create_dir ( & git_dir) . unwrap ( ) ;
1980+
1981+ let manager = create_test_manager ( temp_dir. path ( ) , temp_dir. path ( ) . join ( "submod.toml" ) ) ;
1982+
1983+ let expected_paths: Vec < String > = vec ! [ "path/a" . to_string( ) ] ;
1984+
1985+ let status = manager
1986+ . check_sparse_checkout_status (
1987+ & submodule_path. to_string_lossy ( ) ,
1988+ & expected_paths,
1989+ )
1990+ . unwrap ( ) ;
1991+
1992+ assert_eq ! ( status, SparseStatus :: NotConfigured ) ;
1993+ }
1994+
1995+ #[ test]
1996+ fn test_sparse_checkout_correct ( ) {
1997+ let temp_dir = tempdir ( ) . unwrap ( ) ;
1998+ let submodule_path = temp_dir. path ( ) . join ( "submodule" ) ;
1999+ fs:: create_dir ( & submodule_path) . unwrap ( ) ;
2000+
2001+ // Create .git/info/sparse-checkout with the expected paths
2002+ let info_dir = submodule_path. join ( ".git" ) . join ( "info" ) ;
2003+ fs:: create_dir_all ( & info_dir) . unwrap ( ) ;
2004+ let content = format ! ( "{}\n path/a\n path/b\n " , SPARSE_DENY_ALL ) ;
2005+ fs:: write ( info_dir. join ( "sparse-checkout" ) , content) . unwrap ( ) ;
2006+
2007+ let manager = create_test_manager ( temp_dir. path ( ) , temp_dir. path ( ) . join ( "submod.toml" ) ) ;
2008+
2009+ let expected_paths = vec ! [ "path/a" . to_string( ) , "path/b" . to_string( ) ] ;
2010+
2011+ let status = manager
2012+ . check_sparse_checkout_status (
2013+ & submodule_path. to_string_lossy ( ) ,
2014+ & expected_paths,
2015+ )
2016+ . unwrap ( ) ;
2017+
2018+ assert_eq ! ( status, SparseStatus :: Correct ) ;
2019+ }
2020+
2021+ #[ test]
2022+ fn test_sparse_checkout_correct_with_extras ( ) {
2023+ // When the sparse-checkout file contains all expected paths plus additional
2024+ // ones, the result is still Correct (subset check, not equality).
2025+ let temp_dir = tempdir ( ) . unwrap ( ) ;
2026+ let submodule_path = temp_dir. path ( ) . join ( "submodule" ) ;
2027+ fs:: create_dir ( & submodule_path) . unwrap ( ) ;
2028+
2029+ let info_dir = submodule_path. join ( ".git" ) . join ( "info" ) ;
2030+ fs:: create_dir_all ( & info_dir) . unwrap ( ) ;
2031+ // File has path/a, path/b AND an extra path/c not in expected_paths
2032+ let content = format ! ( "{}\n path/a\n path/b\n path/c\n " , SPARSE_DENY_ALL ) ;
2033+ fs:: write ( info_dir. join ( "sparse-checkout" ) , content) . unwrap ( ) ;
2034+
2035+ let manager = create_test_manager ( temp_dir. path ( ) , temp_dir. path ( ) . join ( "submod.toml" ) ) ;
2036+
2037+ let expected_paths = vec ! [ "path/a" . to_string( ) , "path/b" . to_string( ) ] ;
2038+
2039+ let status = manager
2040+ . check_sparse_checkout_status (
2041+ & submodule_path. to_string_lossy ( ) ,
2042+ & expected_paths,
2043+ )
2044+ . unwrap ( ) ;
2045+
2046+ assert_eq ! ( status, SparseStatus :: Correct ) ;
2047+ }
2048+
2049+ #[ test]
2050+ fn test_sparse_checkout_mismatch ( ) {
2051+ let temp_dir = tempdir ( ) . unwrap ( ) ;
2052+ let submodule_path = temp_dir. path ( ) . join ( "submodule" ) ;
2053+ fs:: create_dir ( & submodule_path) . unwrap ( ) ;
2054+
2055+ // sparse-checkout has only path/a; path/b is expected but absent
2056+ let info_dir = submodule_path. join ( ".git" ) . join ( "info" ) ;
2057+ fs:: create_dir_all ( & info_dir) . unwrap ( ) ;
2058+ let content = format ! ( "{}\n path/a\n " , SPARSE_DENY_ALL ) ;
2059+ fs:: write ( info_dir. join ( "sparse-checkout" ) , content) . unwrap ( ) ;
2060+
2061+ let manager = create_test_manager ( temp_dir. path ( ) , temp_dir. path ( ) . join ( "submod.toml" ) ) ;
2062+
2063+ let expected_paths = vec ! [
2064+ "path/a" . to_string( ) ,
2065+ "path/b" . to_string( ) , // expected but not configured
2066+ ] ;
2067+
2068+ let status = manager
2069+ . check_sparse_checkout_status (
2070+ & submodule_path. to_string_lossy ( ) ,
2071+ & expected_paths,
2072+ )
2073+ . unwrap ( ) ;
2074+
2075+ match status {
2076+ SparseStatus :: Mismatch { expected, actual } => {
2077+ assert_eq ! ( expected, vec![ "path/a" . to_string( ) , "path/b" . to_string( ) ] ) ;
2078+ assert_eq ! ( actual, vec![ "path/a" . to_string( ) ] ) ;
2079+ }
2080+ _ => panic ! ( "Expected Mismatch, got {:?}" , status) ,
2081+ }
2082+ }
2083+ }
0 commit comments