@@ -157,6 +157,195 @@ mod open {
157157 Ok ( ( ) )
158158 }
159159
160+ #[ test]
161+ fn gitlink_target_takes_precedence_over_name_in_git_dir_resolution ( ) -> crate :: Result {
162+ let repo = repo ( "submodule-with-divergent-gitlink" ) ?;
163+ let sm = repo
164+ . submodules ( ) ?
165+ . expect ( "modules present" )
166+ . next ( )
167+ . expect ( "one submodule" ) ;
168+
169+ let git_dir_from_name = sm. git_dir ( ) ?;
170+ assert ! (
171+ git_dir_from_name. ends_with( "modules/outer/inner" ) ,
172+ "the name-derived path is the fallback location, what it should be per submodule configuration/name"
173+ ) ;
174+
175+ let git_dir_from_gitlink = sm. git_dir_try_old_form ( ) ?;
176+ assert ! (
177+ git_dir_from_gitlink. ends_with( "modules/inner" ) ,
178+ "the worktree .git file points at the relocated repository"
179+ ) ;
180+ assert_ne ! (
181+ git_dir_from_gitlink, git_dir_from_name,
182+ "the gitlink target must be authoritative even when it differs from .git/modules/<name>"
183+ ) ;
184+ assert_eq ! (
185+ sm. state( ) ?,
186+ submodule:: State {
187+ repository_exists: true ,
188+ is_old_form: false ,
189+ worktree_checkout: true ,
190+ superproject_configuration: true ,
191+ } ,
192+ "a modern gitlink remains modern even when its target differs from the name-derived path"
193+ ) ;
194+
195+ #[ cfg( feature = "status" ) ]
196+ {
197+ let status = sm. status ( gix:: submodule:: config:: Ignore :: None , false ) ?;
198+ assert_eq ! (
199+ status. is_dirty( ) ,
200+ Some ( false ) ,
201+ "opening the gitlink target avoids a 'phantom' submodule HEAD change"
202+ ) ;
203+ assert_eq ! (
204+ status. checked_out_head_id, status. index_id,
205+ "there are no HEAD changes even though the 'phantom' at modules/outer/inner has its HEAD at @~1"
206+ ) ;
207+
208+ let status = sm. status ( gix:: submodule:: config:: Ignore :: All , false ) ?;
209+ assert_eq ! (
210+ status. state,
211+ submodule:: State {
212+ repository_exists: true ,
213+ is_old_form: false ,
214+ worktree_checkout: true ,
215+ superproject_configuration: true ,
216+ } ,
217+ "ignore=all still follows a parseable divergent gitdir file"
218+ ) ;
219+ }
220+
221+ Ok ( ( ) )
222+ }
223+
224+ #[ test]
225+ fn broken_gitlink_target_is_reported ( ) -> crate :: Result {
226+ let repo = repo ( "submodule-with-missing-gitlink-target" ) ?;
227+ let sm = repo
228+ . submodules ( ) ?
229+ . expect ( "modules present" )
230+ . next ( )
231+ . expect ( "one submodule" ) ;
232+
233+ assert ! ( matches!(
234+ sm. git_dir_try_old_form( ) ,
235+ Err ( submodule:: git_dir_try_old_form:: Error :: InvalidGitDirFileTarget {
236+ target: Some ( target) ,
237+ source: None ,
238+ ..
239+ } ) if target. ends_with( "missing" )
240+ ) ) ;
241+ assert ! ( matches!(
242+ sm. state( ) ,
243+ Err ( submodule:: state:: Error :: GitDirTryOldForm (
244+ submodule:: git_dir_try_old_form:: Error :: InvalidGitDirFileTarget {
245+ target: Some ( target) ,
246+ source: None ,
247+ ..
248+ }
249+ ) ) if target. ends_with( "missing" )
250+ ) ) ;
251+ assert ! ( matches!(
252+ sm. open( ) ,
253+ Err ( submodule:: open:: Error :: GitDir (
254+ submodule:: git_dir_try_old_form:: Error :: InvalidGitDirFileTarget {
255+ target: Some ( target) ,
256+ source: None ,
257+ ..
258+ }
259+ ) ) if target. ends_with( "missing" )
260+ ) ) ;
261+
262+ #[ cfg( feature = "status" ) ]
263+ assert ! (
264+ matches!(
265+ sm. status( gix:: submodule:: config:: Ignore :: None , false ) ,
266+ Err ( submodule:: status:: Error :: State (
267+ submodule:: state:: Error :: GitDirTryOldForm (
268+ submodule:: git_dir_try_old_form:: Error :: InvalidGitDirFileTarget {
269+ target: Some ( target) ,
270+ source: None ,
271+ ..
272+ }
273+ )
274+ ) ) if target. ends_with( "missing" )
275+ ) ,
276+ "ignore=none fails as some submodules can't be opened"
277+ ) ;
278+
279+ #[ cfg( feature = "status" ) ]
280+ {
281+ let status = sm. status ( gix:: submodule:: config:: Ignore :: All , false ) ?;
282+ assert_eq ! (
283+ status. state,
284+ submodule:: State {
285+ repository_exists: false ,
286+ is_old_form: false ,
287+ worktree_checkout: true ,
288+ superproject_configuration: true ,
289+ } ,
290+ "ignore=all does not inspect the broken gitdir target"
291+ ) ;
292+ }
293+
294+ Ok ( ( ) )
295+ }
296+
297+ #[ test]
298+ fn malformed_gitlink_target_is_ignored_by_ignore_all_status ( ) -> crate :: Result {
299+ let repo = repo ( "submodule-with-malformed-gitlink" ) ?;
300+ let sm = repo
301+ . submodules ( ) ?
302+ . expect ( "modules present" )
303+ . next ( )
304+ . expect ( "one submodule" ) ;
305+
306+ assert ! ( matches!(
307+ sm. git_dir_try_old_form( ) ,
308+ Err ( submodule:: git_dir_try_old_form:: Error :: InvalidGitDirFileTarget {
309+ target: None ,
310+ source: Some ( _) ,
311+ ..
312+ } )
313+ ) ) ;
314+
315+ #[ cfg( feature = "status" ) ]
316+ {
317+ assert ! (
318+ matches!(
319+ sm. status( gix:: submodule:: config:: Ignore :: None , false ) ,
320+ Err ( submodule:: status:: Error :: State (
321+ submodule:: state:: Error :: GitDirTryOldForm (
322+ submodule:: git_dir_try_old_form:: Error :: InvalidGitDirFileTarget {
323+ target: None ,
324+ source: Some ( _) ,
325+ ..
326+ }
327+ )
328+ ) )
329+ ) ,
330+ "ignore=none fails as some submodules can't be opened"
331+ ) ;
332+
333+ let status = sm. status ( gix:: submodule:: config:: Ignore :: All , false ) ?;
334+ assert_eq ! (
335+ status. state,
336+ submodule:: State {
337+ repository_exists: true ,
338+ is_old_form: false ,
339+ worktree_checkout: true ,
340+ superproject_configuration: true ,
341+ } ,
342+ "ignore=all does not parse the malformed gitdir file"
343+ ) ;
344+ }
345+
346+ Ok ( ( ) )
347+ }
348+
160349 #[ cfg( feature = "status" ) ]
161350 mod status {
162351 use crate :: { submodule:: repo, util:: hex_to_id} ;
0 commit comments