Skip to content

Commit

Permalink
fix(remote-build): check for shallowly cloned git repos (#4498)
Browse files Browse the repository at this point in the history
Shallow git repos are not designed to be pushed, so the new
remote-builder now raises an error for shallowly cloned repos.
core20 and core22 projects in shallow clone repos fall back
to the legacy remote-builder to retain compatibility.
  • Loading branch information
syu-w authored Dec 15, 2023
1 parent a763c54 commit 1e28514
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 7 deletions.
20 changes: 18 additions & 2 deletions snapcraft/commands/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
GitType,
RemoteBuilder,
get_git_repo_type,
)
from snapcraft.utils import confirm_with_user, get_host_architecture, humanize_list

_CONFIRMATION_PROMPT = (
Expand Down Expand Up @@ -193,13 +198,24 @@ def _run_new_or_fallback_remote_build(self, base: str) -> None:
run_legacy()
return

if is_repo(Path().absolute()):
git_type = get_git_repo_type(Path().absolute())

if git_type == GitType.NORMAL:
emit.debug(
"Running new remote-build because project is in a git repository"
)
self._run_new_remote_build()
return

if git_type == GitType.SHALLOW:
emit.debug("Current git repository is shallow cloned.")
emit.progress(
"Remote build for shallow clones is deprecated "
"and will be removed in core24",
permanent=True,
)
# fall-through to legacy remote-build

emit.debug("Running fallback remote-build")
run_legacy()

Expand Down
13 changes: 12 additions & 1 deletion snapcraft/remote/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,40 @@
LaunchpadHttpsError,
RemoteBuildError,
RemoteBuildFailedError,
RemoteBuildInvalidGitRepoError,
RemoteBuildTimeoutError,
UnsupportedArchitectureError,
)
from .git import GitRepo, is_repo
from .git import (
GitRepo,
GitType,
check_git_repo_for_remote_build,
get_git_repo_type,
is_repo,
)
from .launchpad import LaunchpadClient
from .remote_builder import RemoteBuilder
from .utils import get_build_id, humanize_list, rmtree, validate_architectures
from .worktree import WorkTree

__all__ = [
"check_git_repo_for_remote_build",
"get_build_id",
"get_git_repo_type",
"humanize_list",
"is_repo",
"rmtree",
"validate_architectures",
"AcceptPublicUploadError",
"GitError",
"GitRepo",
"GitType",
"LaunchpadClient",
"LaunchpadHttpsError",
"RemoteBuilder",
"RemoteBuildError",
"RemoteBuildFailedError",
"RemoteBuildInvalidGitRepoError",
"RemoteBuildTimeoutError",
"UnsupportedArchitectureError",
"WorkTree",
Expand Down
13 changes: 13 additions & 0 deletions snapcraft/remote/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,16 @@ def __init__(self, details: str) -> None:
brief = "Remote build failed."

super().__init__(brief=brief, details=details)


class RemoteBuildInvalidGitRepoError(RemoteBuildError):
"""The Git repository is invalid for remote build.
:param brief: Brief description of error.
:param details: Detailed information.
"""

def __init__(self, details: str) -> None:
brief = "The Git repository is invalid for remote build."

super().__init__(brief=brief, details=details)
47 changes: 46 additions & 1 deletion snapcraft/remote/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@
import os
import subprocess
import time
from enum import Enum
from pathlib import Path
from typing import Optional

import pygit2

from .errors import GitError
from .errors import GitError, RemoteBuildInvalidGitRepoError

logger = logging.getLogger(__name__)


class GitType(Enum):
"""Type of git repository."""

INVALID = 0
NORMAL = 1
SHALLOW = 2


def is_repo(path: Path) -> bool:
"""Check if a directory is a git repo.
Expand All @@ -50,6 +59,42 @@ def is_repo(path: Path) -> bool:
) from error


def get_git_repo_type(path: Path) -> GitType:
"""Check if a directory is a git repo and return the type.
:param path: filepath to check
:returns: GitType
"""
if is_repo(path):
repo = pygit2.Repository(path)
if repo.is_shallow:
return GitType.SHALLOW
return GitType.NORMAL

return GitType.INVALID


def check_git_repo_for_remote_build(path: Path) -> None:
"""Check if a directory meets the requirements of doing remote builds.
:param path: filepath to check
:raises RemoteBuildInvalidGitRepoError: if incompatible git repo is found
"""
git_type = get_git_repo_type(path.absolute())

if git_type == GitType.INVALID:
raise RemoteBuildInvalidGitRepoError(
f"Could not find a git repository in {str(path)!r}"
)

if git_type == GitType.SHALLOW:
raise RemoteBuildInvalidGitRepoError(
"Remote build for shallow cloned git repos are no longer supported"
)


class GitRepo:
"""Git repository class."""

Expand Down
3 changes: 3 additions & 0 deletions snapcraft/remote/remote_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pathlib import Path
from typing import List, Optional

from .git import check_git_repo_for_remote_build
from .launchpad import LaunchpadClient
from .utils import get_build_id, humanize_list, validate_architectures
from .worktree import WorkTree
Expand Down Expand Up @@ -55,6 +56,8 @@ def __init__( # noqa: PLR0913 pylint: disable=too-many-arguments
self._project_name = project_name
self._project_dir = project_dir

check_git_repo_for_remote_build(self._project_dir)

if build_id:
self._build_id = build_id
else:
Expand Down
101 changes: 101 additions & 0 deletions tests/unit/commands/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -523,6 +526,104 @@ 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 fall back to legacy remote-build if in a shallow 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",
git_normal_path.absolute().as_uri(),
git_shallow_path.absolute().as_posix(),
],
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_debug("Running fallback remote-build")


@pytest.mark.parametrize(
"create_snapcraft_yaml", CURRENT_BASES - {"core22"}, indirect=True
)
@pytest.mark.usefixtures(
"create_snapcraft_yaml", "mock_confirm", "mock_argv", "use_new_remote_build"
)
def test_run_in_shallow_repo_unsupported(capsys, new_dir):
"""devel / core24 and newer bases run new remote-build in a shallow 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",
git_normal_path.absolute().as_uri(),
git_shallow_path.absolute().as_posix(),
],
check=True,
)

os.chdir(git_shallow_path)

# no exception because run() catches it
ret = cli.run()
assert ret != 0
_, err = capsys.readouterr()

assert "Remote build for shallow cloned git repos are no longer supported" in err


######################
# Architecture tests #
######################
Expand Down
Loading

0 comments on commit 1e28514

Please sign in to comment.