diff --git a/poetry.lock b/poetry.lock index 953e0db8791..1fcec480052 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.0.dev0 and should not be changed by hand. [[package]] name = "anyio" @@ -616,7 +616,7 @@ version = "1.2.1" description = "Python Git Library" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "test"] files = [ {file = "dulwich-1.2.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e45a68da922a4abe8fd015ed020ec0123ee58176a6984a34d2a2c74c959e45d3"}, {file = "dulwich-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e88fd960a9327d87556a3bad76ad84b6346d3a07409fe8f081877a775977c86b"}, @@ -2025,7 +2025,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\"", github-actions = "python_version == \"3.10\"", test = "python_version == \"3.10\""} +markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\"", github-actions = "python_version == \"3.10\"", test = "python_version < \"3.12\""} [[package]] name = "urllib3" @@ -2267,4 +2267,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "aaa48ef867f604f545182fae11b744abdf8c1f4b0d75b47647d598140666a4c3" +content-hash = "8778d2a6e776baa5a18d89a4c3e8e78d5bafbdbcbbc66e1c179373d7eaa0a5ef" diff --git a/pyproject.toml b/pyproject.toml index a32ccb73f1a..e6df28664ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ pytest-cov = ">=4.0" pytest-mock = ">=3.9" pytest-randomly = ">=3.12" pytest-xdist = { version = ">=3.1", extras = ["psutil"] } +dulwich = ">=1.2.1" [tool.poetry.group.typing.dependencies] mypy = ">=1.8.0" diff --git a/tests/vcs/git/test_backend.py b/tests/vcs/git/test_backend.py index 03c02d3ff72..f30c6cd0700 100644 --- a/tests/vcs/git/test_backend.py +++ b/tests/vcs/git/test_backend.py @@ -455,3 +455,79 @@ def test_clone_legacy_strips_ref_prefixes( mock_clone.assert_called_once() mock_checkout.assert_called_once_with(expected_checkout, target) + + +@pytest.mark.skip_git_mock +def test_clone_with_lfs_files(tmp_path: Path) -> None: + """Test cloning a repository with Git LFS files (issue #8723).""" + from dulwich import porcelain + from dulwich.lfs import LFSStore + + # Create a source repository with LFS support + source_path = tmp_path / "source-repo" + source_path.mkdir() + repo = Repo.init(str(source_path)) + + # Set up LFS in the repository + lfs_dir = source_path / ".git" / "lfs" + lfs_dir.mkdir(parents=True) + lfs_store = LFSStore.create(str(lfs_dir)) + + # Configure .gitattributes to track large files with LFS + gitattributes = source_path / ".gitattributes" + gitattributes.write_text("*.bin filter=lfs diff=lfs merge=lfs -text\n") + porcelain.add(repo, str(gitattributes)) + + # Create a regular file + regular_file = source_path / "regular.txt" + regular_file.write_text("This is a regular file") + porcelain.add(repo, str(regular_file)) + + # Create an LFS file with a pointer + lfs_content = b"This is a large binary file content for LFS storage" + lfs_file = source_path / "large.bin" + + # Store the actual content in LFS store and create pointer + lfs_object_id = lfs_store.write_object([lfs_content]) + lfs_pointer = ( + f"version https://git-lfs.github.com/spec/v1\n" + f"oid sha256:{lfs_object_id}\n" + f"size {len(lfs_content)}\n" + ) + lfs_file.write_text(lfs_pointer) + porcelain.add(repo, str(lfs_file)) + + # Commit the files + porcelain.commit( + repo, + message=b"Add files with LFS support", + author=b"Test ", + committer=b"Test ", + ) + + # Clone the repository + source_root_dir = tmp_path / "clone-root" + source_root_dir.mkdir() + Git.clone( + url=source_path.as_uri(), + source_root=source_root_dir, + name="clone-test", + ) + + # Verify the clone succeeded + clone_dir = source_root_dir / "clone-test" + assert (clone_dir / ".git").is_dir() + + # Verify regular file is present + assert (clone_dir / "regular.txt").exists() + assert (clone_dir / "regular.txt").read_text() == "This is a regular file" + + # Verify .gitattributes is present + assert (clone_dir / ".gitattributes").exists() + assert "filter=lfs" in (clone_dir / ".gitattributes").read_text() + + # Verify LFS file is present with actual content (not just pointer) + # The LFS system should automatically retrieve the actual content + assert (clone_dir / "large.bin").exists() + lfs_file_content = (clone_dir / "large.bin").read_bytes() + assert lfs_file_content == lfs_content