Skip to content

Commit

Permalink
RubyGems: Checkout branches for local Git repos
Browse files Browse the repository at this point in the history
Current implementation doesn't support Git dependencies specified with
`branch:` tag, because in order for local Git dependency redirection to
work, branch has to be checked out when it's specified (otherwise it
doesn't have to be).

Signed-off-by: Milan Tichavský <[email protected]>
  • Loading branch information
mtichavsky authored and chmeliik committed Sep 5, 2022
1 parent f9f3db4 commit 7f1c938
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 14 deletions.
4 changes: 4 additions & 0 deletions cachito/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class UnknownHashAlgorithm(CachitoError):
"""The hash algorithm is unknown by Cachito."""


class GitError(CachitoError):
"""An error was encountered during manipulation with a Git repository."""


# Request error classifiers
class ClientError(Exception):
"""Client Error."""
Expand Down
41 changes: 35 additions & 6 deletions cachito/workers/pkg_managers/rubygems.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
import urllib.parse
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

import requests
from gemlock_parser.gemfile_lock import GemfileLockParser
from git import Repo
from git.exc import CheckoutError

from cachito.common.utils import get_repo_name
from cachito.errors import NexusError, ValidationError
from cachito.errors import GitError, NexusError, ValidationError
from cachito.workers import get_worker_config, nexus
from cachito.workers.errors import NexusScriptError, UploadError
from cachito.workers.paths import RequestBundleDir
Expand Down Expand Up @@ -41,6 +44,7 @@ class GemMetadata:
version: str
type: str
source: str
branch: Optional[str] = None


def prepare_nexus_for_rubygems_request(rubygems_repo_name):
Expand Down Expand Up @@ -86,7 +90,7 @@ def parse_gemlock(source_dir, gemlock_path):
continue
_validate_gem_metadata(gem, source_dir, gemlock_path.parent)
source = gem.remote if gem.type != "PATH" else gem.path
dependencies.append(GemMetadata(gem.name, gem.version, gem.type, source))
dependencies.append(GemMetadata(gem.name, gem.version, gem.type, source, gem.branch))

return dependencies

Expand Down Expand Up @@ -331,6 +335,7 @@ def _download_git_package(gem, rubygems_deps_dir, rubygems_raw_repo_name, nexus_
"path": download_path,
"raw_component_name": raw_component_name,
"have_raw_component": have_raw_component,
"branch": gem.branch,
}


Expand Down Expand Up @@ -375,7 +380,7 @@ def resolve_rubygems(package_root, request):

for dep in dependencies:
if dep["kind"] == "GIT":
unpack_git_dependency(dep)
prepare_git_dependency(dep)

name, version = _get_metadata(package_root, request)
if package_root == bundle_dir:
Expand All @@ -389,21 +394,45 @@ def resolve_rubygems(package_root, request):
}


def unpack_git_dependency(dep):
def prepare_git_dependency(dep):
"""
Unpack the archive with the downloaded dependency.
Unpack the archive with the downloaded dependency and checkout a specified Git branch.
Only the unpacked directory is kept, the archive is deleted.
To get more info on local Git repos, see:
https://bundler.io/man/bundle-config.1.html#LOCAL-GIT-REPOS
:param dep: RubyGems GIT dependency
"""
#
extracted_path = str(dep["path"]).removesuffix(".tar.gz")
extracted_path = Path(str(dep["path"]).removesuffix(".tar.gz"))
log.debug(f"Extracting archive at {dep['path']} to {extracted_path}")
shutil.unpack_archive(dep["path"], extracted_path)
os.remove(dep["path"])
dep["path"] = extracted_path

if dep["branch"] is not None:
log.debug(f"Checking out branch {dep['branch']} at {dep['path'] / 'app'}")
checkout_branch(dep)


def checkout_branch(dep: dict):
"""Create and checkout branch dep['branch'] in repository at dep['path']/app.
:param dict dep: GIT dependency with keys `branch` and `path` (Path to the unpacked Git repo)
:raises GitError: If creating Git objects or checking out a given branch failed
"""
try:
repo = Repo(dep["path"] / "app")
git = repo.git
git.checkout("HEAD", b=dep["branch"])
except CheckoutError:
raise GitError(f"Couldn't checkout branch {dep['branch']} at {dep['path'] / 'app'}")
except Exception:
raise GitError(
f"An error occurred during creating a Git repository object or branch checkout at path:"
f" {dep['path'] / 'app'}"
)


def _upload_rubygems_package(repo_name, artifact_path):
"""
Expand Down
49 changes: 41 additions & 8 deletions tests/test_workers/test_pkg_managers/test_rubygems.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
from textwrap import dedent
from unittest import mock

import git
import pytest
import requests

from cachito.errors import NexusError, ValidationError
from cachito.errors import GitError, NexusError, ValidationError
from cachito.workers.errors import NexusScriptError, UploadError
from cachito.workers.pkg_managers import general, rubygems
from cachito.workers.pkg_managers.rubygems import GemMetadata, parse_gemlock
Expand Down Expand Up @@ -487,7 +488,9 @@ def test_download_git_package(
):
raw_url = "https://nexus:8081/repository/cachito-rubygems-raw/json.tar.gz"

dependency = GemMetadata("json", GIT_REF, "GIT", "https://github.com/org/json.git")
dependency = GemMetadata(
"json", GIT_REF, "GIT", "https://github.com/org/json.git", "master"
)

git_archive_path = tmp_path / "json.tar.gz"

Expand All @@ -510,6 +513,7 @@ def test_download_git_package(
),
"raw_component_name": raw_component,
"have_raw_component": have_raw_component,
"branch": "master",
}

assert (
Expand Down Expand Up @@ -706,7 +710,7 @@ def test_resolve_rubygems_invalid_gemfile_lock_path(mock_request_bundle_dir, tmp


@pytest.mark.parametrize("subpath_pkg", [True, False])
@mock.patch("cachito.workers.pkg_managers.rubygems.unpack_git_dependency")
@mock.patch("cachito.workers.pkg_managers.rubygems.prepare_git_dependency")
@mock.patch("cachito.workers.pkg_managers.rubygems._get_metadata")
@mock.patch("cachito.workers.pkg_managers.rubygems._upload_rubygems_package")
@mock.patch("cachito.workers.pkg_managers.rubygems.download_dependencies")
Expand Down Expand Up @@ -892,13 +896,42 @@ def test_get_metadata(mock_request_bundle_dir, tmp_path, package_subpath, expect
assert version == GIT_REF


@pytest.mark.parametrize("branch", [None, "some-branch"])
@mock.patch("cachito.workers.pkg_managers.rubygems.os.remove")
@mock.patch("cachito.workers.pkg_managers.rubygems.shutil.unpack_archive")
def test_unpack_git_dependency(mock_unpack, mock_remove):
original_path = "some/path.tar.gz"
new_path = "some/path"
dep = {"path": original_path}
rubygems.unpack_git_dependency(dep)
@mock.patch("cachito.workers.pkg_managers.rubygems.checkout_branch")
def test_prepare_git_dependency(mock_checkout_branch, mock_unpack, mock_remove, branch):
original_path = Path("some/path.tar.gz")
new_path = Path("some/path")
dep = {"path": original_path, "branch": branch}

rubygems.prepare_git_dependency(dep)

mock_unpack.assert_called_once_with(original_path, new_path)
mock_remove.assert_called_once_with(original_path)
if branch is None:
mock_checkout_branch.assert_not_called()
else:
mock_checkout_branch.assert_called_once()
assert dep["path"] == new_path


@mock.patch("cachito.workers.pkg_managers.rubygems.Repo")
def test_checkout_branch(mock_repo):
rubygems.checkout_branch({"path": Path("/yo"), "branch": "b"})

mock_repo.assert_called_with(Path("/yo/app"))
mock_repo.return_value.git.checkout.assert_called_once_with("HEAD", b="b")


@mock.patch("cachito.workers.pkg_managers.rubygems.Repo")
def test_checkout_branch_raises(mock_repo):
dep = {"path": Path("/yo"), "branch": "b"}
mock_repo.return_value.git.checkout.side_effect = git.exc.CheckoutError

with pytest.raises(GitError):
rubygems.checkout_branch(dep)

mock_repo.side_effect = git.exc.InvalidGitRepositoryError
with pytest.raises(GitError):
rubygems.checkout_branch(dep)

0 comments on commit 7f1c938

Please sign in to comment.