@@ -53,6 +53,138 @@ def _patch_git_config(name, value):
5353 yield
5454
5555
56+ @pytest .mark .parametrize ("state" , ["unborn" , "untracked" , "detached" ])
57+ @pytest .mark .parametrize ("remote" , [None , "." , "origin" ], ids = ["working-tree" , "local-origin" , "origin" ])
58+ def test_add_relative_submodule_without_tracking_remote (tmp_path , state , remote ):
59+ with git .Repo .init (tmp_path / "parent" ) as parent :
60+ if state != "unborn" :
61+ parent .index .commit ("Initial commit" )
62+ if state == "detached" :
63+ parent .head .reference = parent .head .commit
64+ base = tmp_path / "remote" if remote == "origin" else tmp_path / "parent"
65+ if remote is not None :
66+ parent .create_remote ("origin" , str (base ) if remote == "origin" else "." )
67+ with git .Repo .init (base / "source" ) as source :
68+ source .git .symbolic_ref ("HEAD" , "refs/heads/master" )
69+ source .index .commit ("Initial commit" )
70+ submodule = parent .create_submodule ("module" , "module" , "./source" )
71+ assert submodule .url == "./source"
72+ with submodule .module () as module :
73+ assert module .head .commit == source .head .commit
74+ assert Path (module .remotes .origin .url ).samefile (source .working_tree_dir )
75+ parent .index .commit ("Add submodule" )
76+ submodule .remove (configuration = False )
77+ submodule .update (init = True )
78+ with submodule .module () as module :
79+ assert module .head .commit == source .head .commit
80+ assert Path (module .remotes .origin .url ).samefile (source .working_tree_dir )
81+
82+
83+ @pytest .mark .parametrize ("url" , ["./source" , "../source" , "./../source" ])
84+ def test_add_preserves_relative_url_on_cygwin (tmp_path , url ):
85+ with git .Repo .init (tmp_path / "parent" ) as parent , git .Repo .init (tmp_path / "source" ) as source :
86+ source .index .commit ("Initial commit" )
87+ with mock .patch .object (Submodule , "_clone_repo" , return_value = source ):
88+ with mock .patch .object (Git , "is_cygwin" , return_value = True ):
89+ submodule = parent .create_submodule ("module" , "module" , url )
90+ assert submodule .url == url
91+ with submodule .config_reader () as reader :
92+ assert reader .get_value ("url" ) == url
93+ with parent .config_reader () as reader :
94+ assert reader .get_value ('submodule "module"' , "url" ) == url
95+
96+
97+ def test_update_resolves_local_relative_remote_once (tmp_path ):
98+ with git .Repo .init (tmp_path / "parent" ) as parent , git .Repo .init (tmp_path / "repos" / "child.git" ) as source :
99+ source .git .symbolic_ref ("HEAD" , "refs/heads/master" )
100+ source .index .commit ("Initial commit" )
101+ parent .create_remote ("origin" , "../repos/super.git" )
102+ parent .head .reference .set_tracking_branch (git .RemoteReference (parent , "refs/remotes/origin/master" ))
103+ submodule = Submodule (
104+ parent ,
105+ source .head .commit .binsha ,
106+ name = "module" ,
107+ path = "module" ,
108+ url = "../child.git" ,
109+ branch_path = "refs/heads/master" ,
110+ )
111+ submodule .update (init = True )
112+ with submodule .module () as module :
113+ assert module .head .commit == source .head .commit
114+ assert Path (module .remotes .origin .url ).samefile (source .working_tree_dir )
115+ module .remotes .origin .fetch ()
116+
117+
118+ @pytest .mark .parametrize ("no_fetch" , [False , True ])
119+ def test_root_update_anchors_relative_remote_url (tmp_path , no_fetch ):
120+ with git .Repo .init (tmp_path / "repos" / "source.git" ) as source , git .Repo .init (tmp_path / "parent" ) as parent :
121+ source .git .symbolic_ref ("HEAD" , "refs/heads/master" )
122+ source .index .commit ("Initial commit" )
123+ with git .Repo .clone_from (source .working_tree_dir , tmp_path / "repos" / "replacement.git" ) as replacement :
124+ parent .create_remote ("origin" , "../repos/super.git" )
125+ submodule = parent .create_submodule ("module" , "module" , source .working_tree_dir )
126+ with submodule .config_writer () as writer :
127+ writer .set_value ("url" , "../source.git" )
128+ previous = parent .index .commit ("Record relative URL" )
129+ with submodule .config_writer () as writer :
130+ writer .set_value ("url" , "../replacement.git" )
131+ parent .index .commit ("Change relative URL" )
132+
133+ RootModule (parent ).update (previous_commit = previous , no_fetch = no_fetch )
134+
135+ with submodule .module () as module :
136+ assert Path (module .remotes .origin .url ).samefile (replacement .working_tree_dir )
137+ assert module .head .commit == replacement .head .commit
138+ module .remotes .origin .fetch ()
139+
140+
141+ @pytest .mark .parametrize ("no_fetch" , [False , True ])
142+ def test_root_update_uses_single_remote_without_resolving_old_url (tmp_path , no_fetch ):
143+ with git .Repo .init (tmp_path / "source" ) as source , git .Repo .init (tmp_path / "parent" ) as parent :
144+ source .git .symbolic_ref ("HEAD" , "refs/heads/master" )
145+ source .index .commit ("Initial commit" )
146+ with git .Repo .clone_from (source .working_tree_dir , tmp_path / "replacement" ) as replacement :
147+ parent .create_remote ("origin" , "../super.git" )
148+ submodule = parent .create_submodule ("module" , "module" , source .working_tree_dir )
149+ with submodule .config_writer () as writer :
150+ writer .set_value ("url" , "../../lib.git" )
151+ previous = parent .index .commit ("Record historical URL" )
152+ with submodule .config_writer () as writer :
153+ writer .set_value ("url" , replacement .working_tree_dir )
154+ parent .index .commit ("Replace historical URL" )
155+
156+ RootModule (parent ).update (previous_commit = previous , no_fetch = no_fetch )
157+
158+ with submodule .module () as module :
159+ assert Path (module .remotes .origin .url ).samefile (replacement .working_tree_dir )
160+ assert module .head .commit == replacement .head .commit
161+
162+
163+ @pytest .mark .parametrize ("dry_run" , [False , True ])
164+ def test_root_update_skips_old_url_resolution_when_remote_matches (tmp_path , dry_run ):
165+ with git .Repo .init (tmp_path / "source" ) as source , git .Repo .init (tmp_path / "parent" ) as parent :
166+ source .index .commit ("Initial commit" )
167+ submodule = parent .create_submodule ("module" , "module" , source .working_tree_dir )
168+ with submodule .config_writer () as writer :
169+ writer .set_value ("url" , "../source" )
170+ previous = parent .index .commit ("Record relative submodule URL" )
171+ with submodule .config_writer () as writer :
172+ writer .set_value ("url" , Git .polish_url (source .working_tree_dir ))
173+ parent .index .commit ("Use existing remote URL" )
174+
175+ resolve_url = Submodule ._resolved_url
176+
177+ def resolve_current_url (sm ):
178+ assert sm .url != "../source" , "Previous URL does not need resolving"
179+ return resolve_url (sm )
180+
181+ with mock .patch .object (Submodule , "_resolved_url" , autospec = True , side_effect = resolve_current_url ):
182+ RootModule (parent ).update (previous_commit = previous , no_fetch = True , dry_run = dry_run )
183+ with submodule .module () as module :
184+ assert module .remotes .origin .url == Git .polish_url (source .working_tree_dir )
185+ assert module .head .commit == source .head .commit
186+
187+
56188@pytest .mark .parametrize (
57189 "name" , ["module" , "$GITPYTHON_TEST_SECRET" , "prefix-${GITPYTHON_TEST_SECRET}-suffix" , "%GITPYTHON_TEST_SECRET%" ]
58190)
@@ -2437,6 +2569,9 @@ def test_resolve_relative_url_like_git(self):
24372569 ("//share/super" , "../../sub" , "//sub" ),
24382570 ("foo/bar" , "../sub" , "foo/sub" ),
24392571 ("./foo" , "../sub" , "sub" ),
2572+ ("./super.git" , "../" , "" ),
2573+ ("ssh://host/org/super.git" , "./" , "ssh://host/org/super.git/" ),
2574+ ("ssh://host/org/super.git" , "../" , "ssh://host/org/" ),
24402575 ("../foo/bar" , "../sub" , "../foo/sub" ),
24412576 ("ssh://host/org/super.git" , ".sub" , ".sub" ),
24422577 ("ssh://host/org/super.git" , "https://other/sub" , "https://other/sub" ),
@@ -2459,23 +2594,27 @@ def test_update_resolves_relative_url_against_remote(self, rwdir):
24592594 ("../planted" , "ssh://git@host/org/planted" ),
24602595 ]:
24612596 submodule = Submodule (parent , Submodule .NULL_BIN_SHA , name = "module" , path = "module" , url = url )
2462- with mock .patch .object (Submodule , "_clone_repo " , side_effect = AssertionError ("stop" )) as clone_repo :
2597+ with mock .patch .object (git . Repo , "clone_from " , side_effect = AssertionError ("stop" )) as clone_from :
24632598 with pytest .raises (AssertionError , match = "stop" ):
24642599 submodule .update (init = True )
2465- assert clone_repo .call_args .args [1 ] == expected
2600+ assert clone_from .call_args .args [0 ] == expected
24662601
24672602 @with_rw_directory
24682603 def test_clone_repo_resolves_relative_url_against_tracking_remote (self , rwdir ):
24692604 parent = git .Repo .init (osp .join (rwdir , "parent" ))
24702605 parent .index .commit ("initial" )
2471- parent .create_remote ("origin" , "git@host:org/super.git" )
2472- parent .head .reference .set_tracking_branch (git .RemoteReference (parent , "refs/remotes/origin/main" ))
2473-
2474- for url , expected in [
2475- ("./planted" , "git@host:org/super.git/planted" ),
2476- ("../planted" , "git@host:org/planted" ),
2477- ]:
2478- with mock .patch .object (git .Repo , "clone_from" , side_effect = AssertionError ("stop" )) as clone_from :
2479- with pytest .raises (AssertionError , match = "stop" ):
2480- Submodule ._clone_repo (parent , url , "module" , "module" )
2481- assert clone_from .call_args .args [0 ] == expected
2606+ parent .create_remote ("origin" , "git@host:other/super.git" )
2607+ for remote_name in ("upstream" , "team/upstream" ):
2608+ parent .create_remote (remote_name , "git@host:org/super.git" )
2609+ with parent .head .reference .config_writer () as writer :
2610+ writer .set_value ("remote" , remote_name )
2611+ writer .set_value ("merge" , "refs/heads/main" )
2612+
2613+ for url , expected in [
2614+ ("./planted" , "git@host:org/super.git/planted" ),
2615+ ("../planted" , "git@host:org/planted" ),
2616+ ]:
2617+ with mock .patch .object (git .Repo , "clone_from" , side_effect = AssertionError ("stop" )) as clone_from :
2618+ with pytest .raises (AssertionError , match = "stop" ):
2619+ Submodule ._clone_repo (parent , url , "module" , "module" )
2620+ assert clone_from .call_args .args [0 ] == expected
0 commit comments