Skip to content

Commit e3f38ff

Browse files
committed
Move clone tests into dedicated file
1 parent d68efad commit e3f38ff

File tree

2 files changed

+294
-283
lines changed

2 files changed

+294
-283
lines changed

test/test_clone.py

Lines changed: 292 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
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+
416
from pathlib import Path
517
import re
618

719
import git
8-
9-
from test.lib import TestBase, with_rw_directory
20+
import pytest
1021

1122

1223
class 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

Comments
 (0)