diff --git a/snapcraft/commands/remote.py b/snapcraft/commands/remote.py index 3028e274b78..fae410ba762 100644 --- a/snapcraft/commands/remote.py +++ b/snapcraft/commands/remote.py @@ -30,7 +30,12 @@ from snapcraft.errors import MaintenanceBase, SnapcraftError from snapcraft.legacy_cli import run_legacy from snapcraft.parts import yaml_utils -from snapcraft.remote import AcceptPublicUploadError, RemoteBuilder, is_repo +from snapcraft.remote import ( + AcceptPublicUploadError, + RemoteBuilder, + is_repo, + is_shallow_repo, +) from snapcraft.utils import confirm_with_user, get_host_architecture, humanize_list _CONFIRMATION_PROMPT = ( @@ -193,7 +198,27 @@ def _run_new_or_fallback_remote_build(self, base: str) -> None: run_legacy() return - if is_repo(Path().absolute()): + current_path = Path().absolute() + + if is_shallow_repo(current_path): + emit.debug("Current git repository is shallow cloned.") + base = self._get_effective_base() + if base in ["core20", "core22"]: + emit.progress( + "Remote build for shallow clones is deprecated " + "and will be removed in core24", + permanent=True, + ) + emit.progress("Fallback to legacy remote-build", permanent=True) + run_legacy() + return + + raise SnapcraftError( + "remote-build for shallow clones is not supported " + f"for bases newer than core22, current base is {base}" + ) + + if is_repo(current_path): emit.debug( "Running new remote-build because project is in a git repository" ) @@ -228,6 +253,11 @@ def _get_project_name(self) -> str: def _run_new_remote_build(self) -> None: """Run new remote-build code.""" + if is_shallow_repo(Path().absolute()): + raise SnapcraftError( + "remote-build for shallow clones is not supported " + "for bases newer than core22" + ) emit.progress("Setting up launchpad environment") remote_builder = RemoteBuilder( app_name="snapcraft", diff --git a/snapcraft/remote/__init__.py b/snapcraft/remote/__init__.py index c8b74205bed..417e330f19e 100644 --- a/snapcraft/remote/__init__.py +++ b/snapcraft/remote/__init__.py @@ -25,7 +25,7 @@ RemoteBuildTimeoutError, UnsupportedArchitectureError, ) -from .git import GitRepo, is_repo +from .git import GitRepo, is_repo, is_shallow_repo from .launchpad import LaunchpadClient from .remote_builder import RemoteBuilder from .utils import get_build_id, humanize_list, rmtree, validate_architectures @@ -35,6 +35,7 @@ "get_build_id", "humanize_list", "is_repo", + "is_shallow_repo", "rmtree", "validate_architectures", "AcceptPublicUploadError", diff --git a/snapcraft/remote/git.py b/snapcraft/remote/git.py index 6bf914cdc5a..cda16ad4973 100644 --- a/snapcraft/remote/git.py +++ b/snapcraft/remote/git.py @@ -50,6 +50,22 @@ def is_repo(path: Path) -> bool: ) from error +def is_shallow_repo(path: Path) -> bool: + """Check if a directory is a shallow cloned git repo. + + :param path: filepath to check + + :returns: True if path is a git repo. + + :raises GitError: if git fails while checking for a repository + """ + if is_repo(path): + repo = pygit2.Repository(path) + return repo.is_shallow + + return False + + class GitRepo: """Git repository class.""" diff --git a/tests/unit/commands/test_remote.py b/tests/unit/commands/test_remote.py index c8c3688f7cd..f97ed255664 100644 --- a/tests/unit/commands/test_remote.py +++ b/tests/unit/commands/test_remote.py @@ -16,6 +16,9 @@ """Remote-build command tests.""" +import os +import shutil +import subprocess import sys from pathlib import Path from unittest.mock import ANY, call @@ -523,6 +526,101 @@ def test_run_in_repo_newer_than_core22( emitter.assert_debug("Running new remote-build because base is newer than core22") +@pytest.mark.parametrize( + "create_snapcraft_yaml", LEGACY_BASES | {"core22"}, indirect=True +) +@pytest.mark.usefixtures("create_snapcraft_yaml", "mock_confirm", "mock_argv") +def test_run_in_shallow_repo(emitter, mock_run_legacy, new_dir): + """core22 and older bases run new remote-build if in a git repo.""" + root_path = Path(new_dir) + git_normal_path = root_path / "normal" + git_normal_path.mkdir() + git_shallow_path = root_path / "shallow" + + shutil.move(root_path / "snap", git_normal_path) + + repo_normal = GitRepo(git_normal_path) + (repo_normal.path / "1").write_text("1") + repo_normal.add_all() + repo_normal.commit("1") + + (repo_normal.path / "2").write_text("2") + repo_normal.add_all() + repo_normal.commit("2") + + (repo_normal.path / "3").write_text("3") + repo_normal.add_all() + repo_normal.commit("3") + + # pygit2 does not support shallow cloning, so we use git directly + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "file://" + str(git_normal_path.absolute()), + str(git_shallow_path.absolute()), + ], + check=True, + ) + + os.chdir(git_shallow_path) + cli.run() + + mock_run_legacy.assert_called_once() + emitter.assert_debug("Current git repository is shallow cloned.") + emitter.assert_progress("Fallback to legacy remote-build", permanent=True) + + +@pytest.mark.parametrize("create_snapcraft_yaml", {"devel"}, indirect=True) +@pytest.mark.usefixtures( + "create_snapcraft_yaml", "mock_confirm", "mock_argv", "use_new_remote_build" +) +def test_run_in_shallow_repo_unsupported(emitter, new_dir, mock_remote_builder): + """core22 and older bases run new remote-build if in a git repo.""" + root_path = Path(new_dir) + git_normal_path = root_path / "normal" + git_normal_path.mkdir() + git_shallow_path = root_path / "shallow" + + shutil.move(root_path / "snap", git_normal_path) + + repo_normal = GitRepo(git_normal_path) + (repo_normal.path / "1").write_text("1") + repo_normal.add_all() + repo_normal.commit("1") + + (repo_normal.path / "2").write_text("2") + repo_normal.add_all() + repo_normal.commit("2") + + (repo_normal.path / "3").write_text("3") + repo_normal.add_all() + repo_normal.commit("3") + + # pygit2 does not support shallow cloning, so we use git directly + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "file://" + str(git_normal_path.absolute()), + str(git_shallow_path.absolute()), + ], + check=True, + ) + + os.chdir(git_shallow_path) + + # no exception because run() catches it + ret = cli.run() + assert ret != 0 + + mock_remote_builder.assert_not_called() + + ###################### # Architecture tests # ###################### diff --git a/tests/unit/remote/test_git.py b/tests/unit/remote/test_git.py index cb16f28980f..8d3b94eee4c 100644 --- a/tests/unit/remote/test_git.py +++ b/tests/unit/remote/test_git.py @@ -18,13 +18,14 @@ """Tests for the pygit2 wrapper class.""" import re +import subprocess from pathlib import Path from unittest.mock import ANY import pygit2 import pytest -from snapcraft.remote import GitError, GitRepo, is_repo +from snapcraft.remote import GitError, GitRepo, is_repo, is_shallow_repo def test_is_repo(new_dir): @@ -39,6 +40,42 @@ def test_is_not_repo(new_dir): assert not is_repo(new_dir) +def test_is_shallow_repo(new_dir): + """Check if directory is a shallow cloned repo.""" + root_path = Path(new_dir) + git_normal_path = root_path / "normal" + git_normal_path.mkdir() + git_shallow_path = root_path / "shallow" + + repo_normal = GitRepo(git_normal_path) + (repo_normal.path / "1").write_text("1") + repo_normal.add_all() + repo_normal.commit("1") + + (repo_normal.path / "2").write_text("2") + repo_normal.add_all() + repo_normal.commit("2") + + (repo_normal.path / "3").write_text("3") + repo_normal.add_all() + repo_normal.commit("3") + + # pygit2 does not support shallow cloning, so we use git directly + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "file://" + str(git_normal_path.absolute()), + str(git_shallow_path.absolute()), + ], + check=True, + ) + + assert is_shallow_repo(git_shallow_path) + + def test_is_repo_path_only(new_dir): """Only look at the path for a repo.""" Path("parent-repo/not-a-repo/child-repo").mkdir(parents=True)