diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 474850011ab..2f5f550a619 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -33,6 +33,7 @@ from poetry.utils.helpers import remove_directory from poetry.utils.isolated_build import IsolatedBuildError from poetry.utils.isolated_build import IsolatedBuildInstallError +from poetry.vcs.git import Git if TYPE_CHECKING: @@ -46,6 +47,12 @@ from poetry.utils.env import Env +def _package_get_name(package: Package) -> str | None: + if url := package.repository_url: + return Git.get_name_from_source_url(url) + return None + + class Executor: def __init__( self, @@ -167,8 +174,9 @@ def execute(self, operations: list[Operation]) -> int: if is_parallel_unsafe: serial_operations.append(operation) elif operation.package.source_type == "git": - # Git operations on the same repository should be executed serially - serial_git_operations[operation.package.source_url].append( + # Serially execute git operations that get cloned to the same directory, + # to prevent multiple parallel git operations in the same repo. + serial_git_operations[_package_get_name(operation.package)].append( operation ) else: @@ -604,8 +612,6 @@ def _prepare_archive( ) def _prepare_git_archive(self, operation: Install | Update) -> Path: - from poetry.vcs.git import Git - package = operation.package assert package.source_url is not None diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/one/__init__.py b/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/one/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml b/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml new file mode 100644 index 00000000000..1548c3a33a1 --- /dev/null +++ b/tests/fixtures/git/github.com/forked_demo/subdirectories/one-copy/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "one" +version = "1.0.0" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one/one/__init__.py b/tests/fixtures/git/github.com/forked_demo/subdirectories/one/one/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml b/tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml new file mode 100644 index 00000000000..1548c3a33a1 --- /dev/null +++ b/tests/fixtures/git/github.com/forked_demo/subdirectories/one/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "one" +version = "1.0.0" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml b/tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml new file mode 100644 index 00000000000..6a54d8938ff --- /dev/null +++ b/tests/fixtures/git/github.com/forked_demo/subdirectories/two/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetry] +name = "two" +version = "2.0.0" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/git/github.com/forked_demo/subdirectories/two/two/__init__.py b/tests/fixtures/git/github.com/forked_demo/subdirectories/two/two/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/helpers.py b/tests/helpers.py index 491cec88a91..d8211e7fc1a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -113,7 +113,8 @@ def mock_clone( if not source_root: source_root = Path(Config.create().get("cache-dir")) / "src" - dest = source_root / path + assert parsed.name is not None + dest = source_root / parsed.name dest.mkdir(parents=True, exist_ok=True) copy_path(folder, dest) diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index c25d05e0b67..b9905d6af70 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1091,7 +1091,7 @@ def test_executor_should_append_subdirectory_for_git( executor.execute([Install(package)]) archive_arg = spy.call_args[0][0] - assert archive_arg == tmp_venv.path / "src/demo/subdirectories/two" + assert archive_arg == tmp_venv.path / "src/subdirectories/two" def test_executor_should_install_multiple_packages_from_same_git_repository( @@ -1131,10 +1131,52 @@ def test_executor_should_install_multiple_packages_from_same_git_repository( executor.execute([Install(package_a), Install(package_b)]) archive_arg = spy.call_args_list[0][0][0] - assert archive_arg == tmp_venv.path / "src/demo/subdirectories/package_a" + assert archive_arg == tmp_venv.path / "src/subdirectories/package_a" archive_arg = spy.call_args_list[1][0][0] - assert archive_arg == tmp_venv.path / "src/demo/subdirectories/package_b" + assert archive_arg == tmp_venv.path / "src/subdirectories/package_b" + + +def test_executor_should_install_multiple_packages_from_forked_git_repository( + mocker: MockerFixture, + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + wheel: Path, +) -> None: + package_a = Package( + "one", + "1.0.0", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="one", + ) + package_b = Package( + "two", + "2.0.0", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/forked_demo/subdirectories.git", + source_subdirectory="two", + ) + + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") + + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef + executor.execute([Install(package_a), Install(package_b)]) + + # Verify that the repo for package_a is not re-used for package_b. + # both repos must be cloned serially into separate directories. + # If so, executor.prepare() will be called twice. + assert prepare_spy.call_count == 2 def test_executor_should_write_pep610_url_references_for_git_with_subdirectories(