11# This module is part of GitPython and is released under the
22# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
33
4+ import os
5+ import os .path as osp
6+ import pathlib
7+ import sys
8+ import tempfile
9+ from unittest import skip
10+
11+ from git import GitCommandError , Repo
12+ from git .exc import UnsafeOptionError , UnsafeProtocolError
13+
14+ from test .lib import TestBase , with_rw_directory , with_rw_repo
15+
416from pathlib import Path
517import re
618
719import git
8-
9- from test .lib import TestBase , with_rw_directory
20+ import pytest
1021
1122
1223class TestClone (TestBase ):
@@ -29,3 +40,282 @@ def test_checkout_in_non_empty_dir(self, rw_dir):
2940 )
3041 else :
3142 self .fail ("GitCommandError not raised" )
43+
44+ @with_rw_directory
45+ def test_clone_from_pathlib (self , rw_dir ):
46+ original_repo = Repo .init (osp .join (rw_dir , "repo" ))
47+
48+ Repo .clone_from (original_repo .git_dir , pathlib .Path (rw_dir ) / "clone_pathlib" )
49+
50+ @with_rw_directory
51+ def test_clone_from_pathlib_withConfig (self , rw_dir ):
52+ original_repo = Repo .init (osp .join (rw_dir , "repo" ))
53+
54+ cloned = Repo .clone_from (
55+ original_repo .git_dir ,
56+ pathlib .Path (rw_dir ) / "clone_pathlib_withConfig" ,
57+ multi_options = [
58+ "--recurse-submodules=repo" ,
59+ "--config core.filemode=false" ,
60+ "--config submodule.repo.update=checkout" ,
61+ "--config filter.lfs.clean='git-lfs clean -- %f'" ,
62+ ],
63+ allow_unsafe_options = True ,
64+ )
65+
66+ self .assertEqual (cloned .config_reader ().get_value ("submodule" , "active" ), "repo" )
67+ self .assertEqual (cloned .config_reader ().get_value ("core" , "filemode" ), False )
68+ self .assertEqual (cloned .config_reader ().get_value ('submodule "repo"' , "update" ), "checkout" )
69+ self .assertEqual (
70+ cloned .config_reader ().get_value ('filter "lfs"' , "clean" ),
71+ "git-lfs clean -- %f" ,
72+ )
73+
74+ def test_clone_from_with_path_contains_unicode (self ):
75+ with tempfile .TemporaryDirectory () as tmpdir :
76+ unicode_dir_name = "\u0394 "
77+ path_with_unicode = os .path .join (tmpdir , unicode_dir_name )
78+ os .makedirs (path_with_unicode )
79+
80+ try :
81+ Repo .clone_from (
82+ url = self ._small_repo_url (),
83+ to_path = path_with_unicode ,
84+ )
85+ except UnicodeEncodeError :
86+ self .fail ("Raised UnicodeEncodeError" )
87+
88+ @with_rw_directory
89+ @skip (
90+ """The referenced repository was removed, and one needs to set up a new
91+ password controlled repo under the org's control."""
92+ )
93+ def test_leaking_password_in_clone_logs (self , rw_dir ):
94+ password = "fakepassword1234"
95+ try :
96+ Repo .clone_from (
97+ url = "https://fakeuser:{}@fakerepo.example.com/testrepo" .format (password ),
98+ to_path = rw_dir ,
99+ )
100+ except GitCommandError as err :
101+ assert password not in str (err ), "The error message '%s' should not contain the password" % err
102+ # Working example from a blank private project.
103+ Repo .clone_from (
104+ url = "https://gitlab+deploy-token-392045:mLWhVus7bjLsy8xj8q2V@gitlab.com/mercierm/test_git_python" ,
105+ to_path = rw_dir ,
106+ )
107+
108+ @with_rw_repo ("HEAD" )
109+ def test_clone_unsafe_options (self , rw_repo ):
110+ with tempfile .TemporaryDirectory () as tdir :
111+ tmp_dir = pathlib .Path (tdir )
112+ tmp_file = tmp_dir / "pwn"
113+ unsafe_options = [
114+ f"--upload-pack='touch { tmp_file } '" ,
115+ f"-u 'touch { tmp_file } '" ,
116+ "--config=protocol.ext.allow=always" ,
117+ "-c protocol.ext.allow=always" ,
118+ ]
119+ for unsafe_option in unsafe_options :
120+ with self .assertRaises (UnsafeOptionError ):
121+ rw_repo .clone (tmp_dir , multi_options = [unsafe_option ])
122+ assert not tmp_file .exists ()
123+
124+ unsafe_options = [
125+ {"upload-pack" : f"touch { tmp_file } " },
126+ {"u" : f"touch { tmp_file } " },
127+ {"config" : "protocol.ext.allow=always" },
128+ {"c" : "protocol.ext.allow=always" },
129+ ]
130+ for unsafe_option in unsafe_options :
131+ with self .assertRaises (UnsafeOptionError ):
132+ rw_repo .clone (tmp_dir , ** unsafe_option )
133+ assert not tmp_file .exists ()
134+
135+ @pytest .mark .xfail (
136+ sys .platform == "win32" ,
137+ reason = (
138+ "File not created. A separate Windows command may be needed. This and the "
139+ "currently passing test test_clone_unsafe_options must be adjusted in the "
140+ "same way. Until then, test_clone_unsafe_options is unreliable on Windows."
141+ ),
142+ raises = AssertionError ,
143+ )
144+ @with_rw_repo ("HEAD" )
145+ def test_clone_unsafe_options_allowed (self , rw_repo ):
146+ with tempfile .TemporaryDirectory () as tdir :
147+ tmp_dir = pathlib .Path (tdir )
148+ tmp_file = tmp_dir / "pwn"
149+ unsafe_options = [
150+ f"--upload-pack='touch { tmp_file } '" ,
151+ f"-u 'touch { tmp_file } '" ,
152+ ]
153+ for i , unsafe_option in enumerate (unsafe_options ):
154+ destination = tmp_dir / str (i )
155+ assert not tmp_file .exists ()
156+ # The options will be allowed, but the command will fail.
157+ with self .assertRaises (GitCommandError ):
158+ rw_repo .clone (destination , multi_options = [unsafe_option ], allow_unsafe_options = True )
159+ assert tmp_file .exists ()
160+ tmp_file .unlink ()
161+
162+ unsafe_options = [
163+ "--config=protocol.ext.allow=always" ,
164+ "-c protocol.ext.allow=always" ,
165+ ]
166+ for i , unsafe_option in enumerate (unsafe_options ):
167+ destination = tmp_dir / str (i )
168+ assert not destination .exists ()
169+ rw_repo .clone (destination , multi_options = [unsafe_option ], allow_unsafe_options = True )
170+ assert destination .exists ()
171+
172+ @with_rw_repo ("HEAD" )
173+ def test_clone_safe_options (self , rw_repo ):
174+ with tempfile .TemporaryDirectory () as tdir :
175+ tmp_dir = pathlib .Path (tdir )
176+ options = [
177+ "--depth=1" ,
178+ "--single-branch" ,
179+ "-q" ,
180+ ]
181+ for option in options :
182+ destination = tmp_dir / option
183+ assert not destination .exists ()
184+ rw_repo .clone (destination , multi_options = [option ])
185+ assert destination .exists ()
186+
187+ @with_rw_repo ("HEAD" )
188+ def test_clone_from_unsafe_options (self , rw_repo ):
189+ with tempfile .TemporaryDirectory () as tdir :
190+ tmp_dir = pathlib .Path (tdir )
191+ tmp_file = tmp_dir / "pwn"
192+ unsafe_options = [
193+ f"--upload-pack='touch { tmp_file } '" ,
194+ f"-u 'touch { tmp_file } '" ,
195+ "--config=protocol.ext.allow=always" ,
196+ "-c protocol.ext.allow=always" ,
197+ ]
198+ for unsafe_option in unsafe_options :
199+ with self .assertRaises (UnsafeOptionError ):
200+ Repo .clone_from (rw_repo .working_dir , tmp_dir , multi_options = [unsafe_option ])
201+ assert not tmp_file .exists ()
202+
203+ unsafe_options = [
204+ {"upload-pack" : f"touch { tmp_file } " },
205+ {"u" : f"touch { tmp_file } " },
206+ {"config" : "protocol.ext.allow=always" },
207+ {"c" : "protocol.ext.allow=always" },
208+ ]
209+ for unsafe_option in unsafe_options :
210+ with self .assertRaises (UnsafeOptionError ):
211+ Repo .clone_from (rw_repo .working_dir , tmp_dir , ** unsafe_option )
212+ assert not tmp_file .exists ()
213+
214+ @pytest .mark .xfail (
215+ sys .platform == "win32" ,
216+ reason = (
217+ "File not created. A separate Windows command may be needed. This and the "
218+ "currently passing test test_clone_from_unsafe_options must be adjusted in the "
219+ "same way. Until then, test_clone_from_unsafe_options is unreliable on Windows."
220+ ),
221+ raises = AssertionError ,
222+ )
223+ @with_rw_repo ("HEAD" )
224+ def test_clone_from_unsafe_options_allowed (self , rw_repo ):
225+ with tempfile .TemporaryDirectory () as tdir :
226+ tmp_dir = pathlib .Path (tdir )
227+ tmp_file = tmp_dir / "pwn"
228+ unsafe_options = [
229+ f"--upload-pack='touch { tmp_file } '" ,
230+ f"-u 'touch { tmp_file } '" ,
231+ ]
232+ for i , unsafe_option in enumerate (unsafe_options ):
233+ destination = tmp_dir / str (i )
234+ assert not tmp_file .exists ()
235+ # The options will be allowed, but the command will fail.
236+ with self .assertRaises (GitCommandError ):
237+ Repo .clone_from (
238+ rw_repo .working_dir , destination , multi_options = [unsafe_option ], allow_unsafe_options = True
239+ )
240+ assert tmp_file .exists ()
241+ tmp_file .unlink ()
242+
243+ unsafe_options = [
244+ "--config=protocol.ext.allow=always" ,
245+ "-c protocol.ext.allow=always" ,
246+ ]
247+ for i , unsafe_option in enumerate (unsafe_options ):
248+ destination = tmp_dir / str (i )
249+ assert not destination .exists ()
250+ Repo .clone_from (
251+ rw_repo .working_dir , destination , multi_options = [unsafe_option ], allow_unsafe_options = True
252+ )
253+ assert destination .exists ()
254+
255+ @with_rw_repo ("HEAD" )
256+ def test_clone_from_safe_options (self , rw_repo ):
257+ with tempfile .TemporaryDirectory () as tdir :
258+ tmp_dir = pathlib .Path (tdir )
259+ options = [
260+ "--depth=1" ,
261+ "--single-branch" ,
262+ "-q" ,
263+ ]
264+ for option in options :
265+ destination = tmp_dir / option
266+ assert not destination .exists ()
267+ Repo .clone_from (rw_repo .common_dir , destination , multi_options = [option ])
268+ assert destination .exists ()
269+
270+ def test_clone_from_unsafe_protocol (self ):
271+ with tempfile .TemporaryDirectory () as tdir :
272+ tmp_dir = pathlib .Path (tdir )
273+ tmp_file = tmp_dir / "pwn"
274+ urls = [
275+ f"ext::sh -c touch% { tmp_file } " ,
276+ "fd::17/foo" ,
277+ ]
278+ for url in urls :
279+ with self .assertRaises (UnsafeProtocolError ):
280+ Repo .clone_from (url , tmp_dir / "repo" )
281+ assert not tmp_file .exists ()
282+
283+ def test_clone_from_unsafe_protocol_allowed (self ):
284+ with tempfile .TemporaryDirectory () as tdir :
285+ tmp_dir = pathlib .Path (tdir )
286+ tmp_file = tmp_dir / "pwn"
287+ urls = [
288+ f"ext::sh -c touch% { tmp_file } " ,
289+ "fd::/foo" ,
290+ ]
291+ for url in urls :
292+ # The URL will be allowed into the command, but the command will
293+ # fail since we don't have that protocol enabled in the Git config file.
294+ with self .assertRaises (GitCommandError ):
295+ Repo .clone_from (url , tmp_dir / "repo" , allow_unsafe_protocols = True )
296+ assert not tmp_file .exists ()
297+
298+ def test_clone_from_unsafe_protocol_allowed_and_enabled (self ):
299+ with tempfile .TemporaryDirectory () as tdir :
300+ tmp_dir = pathlib .Path (tdir )
301+ tmp_file = tmp_dir / "pwn"
302+ urls = [
303+ f"ext::sh -c touch% { tmp_file } " ,
304+ ]
305+ allow_ext = [
306+ "--config=protocol.ext.allow=always" ,
307+ ]
308+ for url in urls :
309+ # The URL will be allowed into the command, and the protocol is enabled,
310+ # but the command will fail since it can't read from the remote repo.
311+ assert not tmp_file .exists ()
312+ with self .assertRaises (GitCommandError ):
313+ Repo .clone_from (
314+ url ,
315+ tmp_dir / "repo" ,
316+ multi_options = allow_ext ,
317+ allow_unsafe_protocols = True ,
318+ allow_unsafe_options = True ,
319+ )
320+ assert tmp_file .exists ()
321+ tmp_file .unlink ()
0 commit comments