Skip to content

Commit 60ec820

Browse files
Vector Lipsss
Vector Li
andcommitted
Retry the git clone action multiple times
Signed-off-by: Vector Li <[email protected]> Co-authored-by: Petr Šplíchal <[email protected]>
1 parent 3c4a56f commit 60ec820

File tree

6 files changed

+118
-37
lines changed

6 files changed

+118
-37
lines changed

docs/overview.rst

+11
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,17 @@ TMT_GIT_CREDENTIALS_URL_<suffix>, TMT_GIT_CREDENTIALS_VALUE_<suffix>
419419
__ https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#clone-repository-using-personal-access-token
420420
__ https://github.blog/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/
421421

422+
TMT_GIT_CLONE_ATTEMPTS
423+
The maximum number of retries to clone a git repository if it
424+
fails. By default, 3 attempts are done.
425+
426+
TMT_GIT_CLONE_INTERVAL
427+
The interval (in seconds) to retry clonning a git repository
428+
again, 10 seconds by default.
429+
430+
TMT_GIT_CLONE_TIMEOUT
431+
Overall maximum time in seconds to clone a git repository. By
432+
default, the limit is not set.
422433

423434
.. _step-variables:
424435

tmt/base.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -2327,7 +2327,10 @@ def import_plan(self) -> Optional['Plan']:
23272327
if destination.exists():
23282328
self.debug(f"Seems that '{destination}' has been already cloned.", level=3)
23292329
else:
2330-
tmt.utils.git_clone(plan_id.url, destination, self)
2330+
tmt.utils.git_clone(
2331+
url=plan_id.url,
2332+
destination=destination,
2333+
logger=self._logger)
23312334
if plan_id.ref:
23322335
# Attempt to evaluate dynamic reference
23332336
try:
@@ -2355,7 +2358,12 @@ def import_plan(self) -> Optional['Plan']:
23552358
self.debug(f"Not enough data to evaluate dynamic ref '{plan_id.ref}', "
23562359
"going to clone the repository to read dynamic ref definition.")
23572360
with tempfile.TemporaryDirectory() as tmpdirname:
2358-
git_clone(str(plan_id.url), Path(tmpdirname), self, None, shallow=True)
2361+
git_clone(
2362+
url=str(plan_id.url),
2363+
destination=Path(tmpdirname),
2364+
shallow=True,
2365+
env=None,
2366+
logger=self._logger)
23592367
self.run(Command(
23602368
'git', 'checkout', 'HEAD', str(plan_id.ref)[1:]),
23612369
cwd=Path(tmpdirname))

tmt/libraries/beakerlib.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,13 @@ def fetch(self) -> None:
217217
with TemporaryDirectory() as tmp:
218218
assert self.url is not None # narrow type
219219
try:
220-
tmt.utils.git_clone(self.url, Path(tmp), self.parent,
221-
env={"GIT_ASKPASS": "echo"}, shallow=True)
222-
except tmt.utils.RunError:
220+
tmt.utils.git_clone(
221+
url=self.url,
222+
destination=Path(tmp),
223+
shallow=True,
224+
env={"GIT_ASKPASS": "echo"},
225+
logger=self._logger)
226+
except (tmt.utils.RunError, tmt.utils.RetryError):
223227
self.parent.debug(f"Repository '{self.url}' not found.")
224228
self._nonexistent_url.add(self.url)
225229
raise LibraryError
@@ -257,8 +261,11 @@ def fetch(self) -> None:
257261
# minimize data transfers if ref is not provided
258262
if not clone_dir.exists():
259263
tmt.utils.git_clone(
260-
self.url, clone_dir, self.parent,
261-
env={"GIT_ASKPASS": "echo"}, shallow=self.ref is None)
264+
url=self.url,
265+
destination=clone_dir,
266+
shallow=self.ref is None,
267+
env={"GIT_ASKPASS": "echo"},
268+
logger=self._logger)
262269

263270
# Detect the default branch from the origin
264271
try:
@@ -339,7 +346,7 @@ def fetch(self) -> None:
339346
self._merge_metadata(library_path, local_library_path)
340347
# Copy fmf metadata
341348
shutil.copytree(self.path / '.fmf', directory / '.fmf', dirs_exist_ok=True)
342-
except (tmt.utils.RunError, tmt.utils.GitUrlError) as error:
349+
except (tmt.utils.RunError, tmt.utils.RetryError, tmt.utils.GitUrlError) as error:
343350
assert self.url is not None
344351
# Fallback to install during the prepare step if in rpm format
345352
if self.format == 'rpm':

tmt/steps/discover/fmf.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,11 @@ def assert_git_url(plan_name: Optional[str] = None) -> None:
357357
# Shallow clone to speed up testing and
358358
# minimize data transfers if ref is not provided
359359
tmt.utils.git_clone(
360-
url, self.testdir, self, env={"GIT_ASKPASS": "echo"}, shallow=ref is None)
360+
url=url,
361+
destination=self.testdir,
362+
shallow=ref is None,
363+
env={"GIT_ASKPASS": "echo"},
364+
logger=self._logger)
361365
git_root = self.testdir
362366
# Copy git repository root to workdir
363367
else:

tmt/steps/discover/shell.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,9 @@ def fetch_remote_repository(
275275
tmt.utils.git_clone(
276276
url=url,
277277
destination=testdir,
278-
common=self,
278+
shallow=ref is None,
279279
env={"GIT_ASKPASS": "echo"},
280-
shallow=ref is None)
280+
logger=self._logger)
281281

282282
# Resolve possible dynamic references
283283
try:

tmt/utils.py

+77-26
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ def is_pidfile(cls, exit_code: Optional[int]) -> bool:
195195
DEFAULT_WAIT_TICK: float = 30.0
196196
DEFAULT_WAIT_TICK_INCREASE: float = 1.0
197197

198+
# Defaults for GIT attempts and interval
199+
DEFAULT_GIT_CLONE_ATTEMPTS: int = 3
200+
DEFAULT_GIT_CLONE_INTERVAL: int = 10
201+
198202
# A stand-in variable for generic use.
199203
T = TypeVar('T')
200204

@@ -1777,7 +1781,7 @@ class RetryError(GeneralError):
17771781
""" Retries unsuccessful """
17781782

17791783
def __init__(self, label: str, causes: list[Exception]) -> None:
1780-
super().__init__(f"Retries of {label} unsuccessful.", causes)
1784+
super().__init__(f"Retries of '{label}' unsuccessful.", causes)
17811785

17821786

17831787
# Step exceptions
@@ -4868,41 +4872,88 @@ def distgit_download(
48684872

48694873

48704874
def git_clone(
4875+
*,
48714876
url: str,
48724877
destination: Path,
4873-
common: Common,
4874-
env: Optional[EnvironmentType] = None,
48754878
shallow: bool = False,
48764879
can_change: bool = True,
4877-
) -> CommandOutput:
4878-
"""
4879-
Git clone url to destination, retry without shallow if necessary
4880+
env: Optional[EnvironmentType] = None,
4881+
attempts: Optional[int] = None,
4882+
interval: Optional[int] = None,
4883+
timeout: Optional[int] = None,
4884+
logger: tmt.log.Logger) -> CommandOutput:
4885+
"""
4886+
Clone git repository from provided url to the destination directory
4887+
4888+
:param url: Source URL of the git repository.
4889+
:param destination: Full path to the destination directory.
4890+
:param shallow: For ``shallow=True`` first try to clone repository
4891+
using ``--depth=1`` option. If not successful clone repo with
4892+
the whole history.
4893+
:param can_change: URL can be modified with hardcoded rules. Use
4894+
``can_change=False`` to disable rewrite rules.
4895+
:param env: Environment provided to the ``git clone`` process.
4896+
:param attempts: Number of tries to call the function.
4897+
:param interval: Amount of seconds to wait before a new try.
4898+
:param timeout: Overall maximum time in seconds to clone the repo.
4899+
:param logger: A Logger instance to be used for logging.
4900+
:returns: Command output, bundled in a :py:class:`CommandOutput` tuple.
4901+
"""
4902+
4903+
def get_env(env: str, default_value: Optional[int]) -> Optional[int]:
4904+
""" Check environment variable, convert to integer """
4905+
value = os.getenv(env, None)
4906+
if value is None:
4907+
return default_value
4908+
try:
4909+
return int(value)
4910+
except ValueError:
4911+
raise GeneralError(f"Invalid '{env}' value, should be 'int', got '{value}'.")
48804912

4881-
For shallow=True attempt to clone repository using --depth=1 option first.
4882-
If not successful attempt to clone whole repo.
4913+
def clone_the_repo(
4914+
url: str,
4915+
destination: Path,
4916+
shallow: bool = False,
4917+
env: Optional[EnvironmentType] = None,
4918+
timeout: Optional[int] = None) -> CommandOutput:
4919+
""" Clone the repo, handle history depth """
48834920

4884-
Common instance is used to run the command for appropriate logging.
4885-
Environment is updated by 'env' dictionary.
4921+
depth = ['--depth=1'] if shallow else []
4922+
return Command('git', 'clone', *depth, url, destination).run(
4923+
cwd=Path('/'), env=env, timeout=timeout, logger=logger)
48864924

4887-
Url can be modified with hardcode rules unless can_change=False is set.
4888-
"""
4889-
depth = ['--depth=1'] if shallow else []
4925+
timeout = timeout or get_env('TMT_GIT_CLONE_TIMEOUT', None)
4926+
attempts = attempts or cast(int, get_env('TMT_GIT_CLONE_ATTEMPTS', DEFAULT_GIT_CLONE_ATTEMPTS))
4927+
interval = interval or cast(int, get_env('TMT_GIT_CLONE_INTERVAL', DEFAULT_GIT_CLONE_INTERVAL))
48904928

4929+
# Update url only once
48914930
if can_change:
48924931
url = clonable_git_url(url)
4893-
try:
4894-
return common.run(
4895-
Command(
4896-
'git', 'clone',
4897-
*depth,
4898-
url, destination
4899-
), env=env)
4900-
except RunError:
4901-
if not shallow:
4902-
# Do not retry if shallow was not used
4903-
raise
4904-
# Git server might not support shallow cloning, try again (do not modify url)
4905-
return git_clone(url, destination, common, env, shallow=False, can_change=False)
4932+
4933+
# Do an extra shallow clone first
4934+
if shallow:
4935+
try:
4936+
return clone_the_repo(
4937+
shallow=True,
4938+
url=url,
4939+
destination=destination,
4940+
env=env,
4941+
timeout=timeout)
4942+
except RunError:
4943+
logger.debug(f"Shallow clone of '{url}' failed, let's try with the full history.")
4944+
4945+
# Finish with whatever number attempts requested (deep)
4946+
return retry(
4947+
func=clone_the_repo,
4948+
attempts=attempts,
4949+
interval=interval,
4950+
label=f"git clone {url} {destination}",
4951+
url=url,
4952+
destination=destination,
4953+
shallow=False,
4954+
env=env,
4955+
timeout=timeout,
4956+
logger=logger)
49064957

49074958

49084959
# ignore[type-arg]: base class is a generic class, but we cannot list its parameter type, because

0 commit comments

Comments
 (0)