kodit 0.2.6__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kodit might be problematic. Click here for more details.
- {kodit-0.2.6 → kodit-0.2.7}/PKG-INFO +1 -1
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/_version.py +2 -2
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/git/factory.py +12 -6
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/git/working_copy.py +12 -4
- kodit-0.2.7/src/kodit/infrastructure/git/git_utils.py +87 -0
- kodit-0.2.7/tests/kodit/infrastructure/cloning/git/factory_test.py +225 -0
- kodit-0.2.7/tests/kodit/infrastructure/cloning/git/working_copy_test.py +168 -0
- kodit-0.2.7/tests/kodit/infrastructure/git/test_git_utils.py +127 -0
- kodit-0.2.6/src/kodit/infrastructure/git/git_utils.py +0 -24
- {kodit-0.2.6 → kodit-0.2.7}/.cursor/rules/kodit.mdc +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.dockerignore +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/CODE_OF_CONDUCT.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/CONTRIBUTING.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/dependabot.yml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/docker.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/docs.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/pull_request.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/pypi-test.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/pypi.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/test.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.gitignore +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.python-version +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.vscode/launch.json +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/.vscode/settings.json +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/Dockerfile +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/LICENSE +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/README.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/alembic.ini +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/_index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/demos/_index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/demos/go-simple-microservice/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/demos/knock-knock-auth/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/developer/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/_index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/installation/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/integration/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/quick-start/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/_index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/configuration/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/deployment/docker-compose.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/deployment/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/deployment/kubernetes.yaml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/indexing/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/docs/reference/telemetry/index.md +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/pyproject.toml +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/.gitignore +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/app.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/commands/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/commands/snippet_commands.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/services/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/services/indexing_application_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/services/snippet_application_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/cli.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/config.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/database.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/entities.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/enums.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/errors.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/interfaces.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/repositories.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/bm25_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/embedding_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/enrichment_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/ignore_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/indexing_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/snippet_extraction_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/source_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/value_objects.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/bm25_factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/local_bm25_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/vectorchord_bm25_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/folder/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/folder/factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/folder/working_copy.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/git/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/metadata.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/batching.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/hash_embedding_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/local_embedding_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/openai_embedding_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/local_vector_search_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/vectorchord_vector_search_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/enrichment_factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/legacy_enrichment_models.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/local_enrichment_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/openai_enrichment_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/git/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ignore/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ignore/ignore_pattern_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/fusion_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/index_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/indexing_factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/language_detection_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/csharp.scm +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/go.scm +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/javascript.scm +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/python.scm +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/typescript.scm +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/snippet_extraction_factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/snippet_query_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/tree_sitter_snippet_extractor.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/embedding_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/file_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/snippet_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ui/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ui/progress.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ui/spinner.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/log.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/mcp.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/middleware.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/README +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/env.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/script.py.mako +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/7c3bbc2ab32b_add_embeddings_table.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/85155663351e_initial.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/9e53ea8bb3b0_add_authors.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/c3f5137d30f5_index_all_the_things.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/src/kodit/reporting.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/conftest.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/docker-smoke.sh +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/cline-prompt-regression-tests/cline_prompt.txt +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/cline-prompt-regression-tests/cline_prompt_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/embedding.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/similarity_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/application/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/application/indexing_application_service_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/application/snippet_application_service_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/cli_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/bm25_domain_service_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/enrichment_domain_service_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/snippet_extraction_domain_service_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/test_embedding_service.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/test_models.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/e2e.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_factory_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_provider/test_hash_embedding_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_provider/test_local_embedding_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_provider/test_openai_embedding_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_batching.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_embedding_integration.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_local_vector_search_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_vectorchord_vector_search_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/test_local_enrichment_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/test_null_enrichment_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/test_openai_enrichment_provider.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/test_enrichment_factory.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/test_enrichment_integration.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/indexing/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/indexing/indexing_repository_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/csharp.cs +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/golang.go +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/javascript.js +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/knock-knock-server.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/python.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/typescript.tsx +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/source/__init__.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/source/source_service_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/sqlalchemy/test_embedding_repository.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/log_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/mcp_test.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/performance/similarity.py +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/smoke.sh +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/tests/vectorchord-smoke.sh +0 -0
- {kodit-0.2.6 → kodit-0.2.7}/uv.lock +0 -0
|
@@ -17,6 +17,7 @@ from kodit.infrastructure.cloning.metadata import (
|
|
|
17
17
|
GitAuthorExtractor,
|
|
18
18
|
GitFileMetadataExtractor,
|
|
19
19
|
)
|
|
20
|
+
from kodit.infrastructure.git.git_utils import sanitize_git_url
|
|
20
21
|
from kodit.infrastructure.ignore.ignore_pattern_provider import GitIgnorePatternProvider
|
|
21
22
|
|
|
22
23
|
|
|
@@ -46,28 +47,33 @@ class GitSourceFactory:
|
|
|
46
47
|
progress_callback = NullProgressCallback()
|
|
47
48
|
|
|
48
49
|
# Normalize the URI
|
|
49
|
-
|
|
50
|
+
# Never log the raw URI in production
|
|
51
|
+
self.log.debug("Normalising git uri", uri="[REDACTED]" + uri[-4:])
|
|
50
52
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
51
53
|
git.Repo.clone_from(uri, temp_dir)
|
|
52
54
|
remote = git.Repo(temp_dir).remote()
|
|
53
55
|
uri = remote.url
|
|
54
56
|
|
|
57
|
+
# Sanitize the URI to remove any credentials
|
|
58
|
+
sanitized_uri = sanitize_git_url(uri)
|
|
59
|
+
self.log.debug("Sanitized git uri", sanitized_uri=sanitized_uri)
|
|
60
|
+
|
|
55
61
|
# Check if source already exists
|
|
56
|
-
self.log.debug("Checking if source already exists", uri=
|
|
57
|
-
source = await self.repository.get_by_uri(
|
|
62
|
+
self.log.debug("Checking if source already exists", uri=sanitized_uri)
|
|
63
|
+
source = await self.repository.get_by_uri(sanitized_uri)
|
|
58
64
|
|
|
59
65
|
if source:
|
|
60
66
|
self.log.info("Source already exists, reusing...", source_id=source.id)
|
|
61
67
|
return source
|
|
62
68
|
|
|
63
|
-
# Prepare working copy
|
|
69
|
+
# Prepare working copy (use original URI for cloning, sanitized for storage)
|
|
64
70
|
clone_path = await self.working_copy.prepare(uri)
|
|
65
71
|
|
|
66
72
|
# Create source record
|
|
67
|
-
self.log.debug("Creating source", uri=
|
|
73
|
+
self.log.debug("Creating source", uri=sanitized_uri, clone_path=str(clone_path))
|
|
68
74
|
source = await self.repository.save(
|
|
69
75
|
Source(
|
|
70
|
-
uri=
|
|
76
|
+
uri=sanitized_uri,
|
|
71
77
|
cloned_path=str(clone_path),
|
|
72
78
|
source_type=SourceType.GIT,
|
|
73
79
|
)
|
|
@@ -5,6 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
import git
|
|
6
6
|
import structlog
|
|
7
7
|
|
|
8
|
+
from kodit.infrastructure.git.git_utils import sanitize_git_url
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class GitWorkingCopyProvider:
|
|
10
12
|
"""Working copy provider for git-based sources."""
|
|
@@ -16,17 +18,23 @@ class GitWorkingCopyProvider:
|
|
|
16
18
|
|
|
17
19
|
async def prepare(self, uri: str) -> Path:
|
|
18
20
|
"""Prepare a Git working copy."""
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
+
# Sanitize the URI for directory name to prevent credential leaks
|
|
22
|
+
sanitized_uri = sanitize_git_url(uri)
|
|
23
|
+
|
|
24
|
+
# Create a unique directory name for the clone using the sanitized URI
|
|
25
|
+
clone_path = self.clone_dir / sanitized_uri.replace("/", "_").replace(":", "_")
|
|
21
26
|
clone_path.mkdir(parents=True, exist_ok=True)
|
|
22
27
|
|
|
23
28
|
try:
|
|
24
|
-
self.log.info(
|
|
29
|
+
self.log.info(
|
|
30
|
+
"Cloning repository", uri=sanitized_uri, clone_path=str(clone_path)
|
|
31
|
+
)
|
|
32
|
+
# Use the original URI for cloning (with credentials if present)
|
|
25
33
|
git.Repo.clone_from(uri, clone_path)
|
|
26
34
|
except git.GitCommandError as e:
|
|
27
35
|
if "already exists and is not an empty directory" not in str(e):
|
|
28
36
|
msg = f"Failed to clone repository: {e}"
|
|
29
37
|
raise ValueError(msg) from e
|
|
30
|
-
self.log.info("Repository already exists, reusing...", uri=
|
|
38
|
+
self.log.info("Repository already exists, reusing...", uri=sanitized_uri)
|
|
31
39
|
|
|
32
40
|
return clone_path
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Git utilities for infrastructure operations."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from urllib.parse import urlparse, urlunparse
|
|
5
|
+
|
|
6
|
+
import git
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_valid_clone_target(target: str) -> bool:
|
|
10
|
+
"""Return True if the target is clonable.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
target: The git repository URL or path to validate.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
True if the target can be cloned, False otherwise.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
20
|
+
try:
|
|
21
|
+
git.Repo.clone_from(target, temp_dir)
|
|
22
|
+
except git.GitCommandError:
|
|
23
|
+
return False
|
|
24
|
+
else:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def sanitize_git_url(url: str) -> str:
|
|
29
|
+
"""Remove credentials from a git URL while preserving the rest of the URL structure.
|
|
30
|
+
|
|
31
|
+
This function handles various git URL formats:
|
|
32
|
+
- HTTPS URLs with username:password@host
|
|
33
|
+
- HTTPS URLs with username@host (no password)
|
|
34
|
+
- SSH URLs (left unchanged)
|
|
35
|
+
- File URLs (left unchanged)
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
url: The git URL that may contain credentials.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The sanitized URL with credentials removed.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
>>> sanitize_git_url("https://phil:token@dev.azure.com/org/project/_git/repo")
|
|
45
|
+
"https://dev.azure.com/org/project/_git/repo"
|
|
46
|
+
>>> sanitize_git_url("https://username@github.com/user/repo.git")
|
|
47
|
+
"https://github.com/user/repo.git"
|
|
48
|
+
>>> sanitize_git_url("git@github.com:user/repo.git")
|
|
49
|
+
"git@github.com:user/repo.git"
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
# Handle SSH URLs (they don't have credentials in the URL format)
|
|
53
|
+
if url.startswith(("git@", "ssh://")):
|
|
54
|
+
return url
|
|
55
|
+
|
|
56
|
+
# Handle file URLs
|
|
57
|
+
if url.startswith("file://"):
|
|
58
|
+
return url
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Parse the URL
|
|
62
|
+
parsed = urlparse(url)
|
|
63
|
+
|
|
64
|
+
# If there are no credentials, return the URL as-is
|
|
65
|
+
if not parsed.username:
|
|
66
|
+
return url
|
|
67
|
+
|
|
68
|
+
# Reconstruct the URL without credentials
|
|
69
|
+
# Keep scheme, netloc (without username/password), path, params, query, fragment
|
|
70
|
+
sanitized_netloc = parsed.hostname
|
|
71
|
+
if parsed.port:
|
|
72
|
+
sanitized_netloc = f"{parsed.hostname}:{parsed.port}"
|
|
73
|
+
|
|
74
|
+
return urlunparse(
|
|
75
|
+
(
|
|
76
|
+
parsed.scheme,
|
|
77
|
+
sanitized_netloc,
|
|
78
|
+
parsed.path,
|
|
79
|
+
parsed.params,
|
|
80
|
+
parsed.query,
|
|
81
|
+
parsed.fragment,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
except Exception: # noqa: BLE001
|
|
86
|
+
# If URL parsing fails, return the original URL
|
|
87
|
+
return url
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Tests for the git source factory module."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import git
|
|
8
|
+
import pytest
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from kodit.domain.entities import Source, SourceType
|
|
12
|
+
from kodit.domain.interfaces import NullProgressCallback
|
|
13
|
+
from kodit.domain.repositories import SourceRepository
|
|
14
|
+
from kodit.infrastructure.cloning.git.factory import GitSourceFactory
|
|
15
|
+
from kodit.infrastructure.cloning.git.working_copy import GitWorkingCopyProvider
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_repository() -> AsyncMock:
|
|
20
|
+
"""Create a mock repository."""
|
|
21
|
+
return AsyncMock(spec=SourceRepository)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def mock_working_copy() -> AsyncMock:
|
|
26
|
+
"""Create a mock working copy provider."""
|
|
27
|
+
return AsyncMock(spec=GitWorkingCopyProvider)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def mock_session() -> AsyncMock:
|
|
32
|
+
"""Create a mock database session."""
|
|
33
|
+
return AsyncMock(spec=AsyncSession)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def factory(
|
|
38
|
+
mock_repository: AsyncMock,
|
|
39
|
+
mock_working_copy: AsyncMock,
|
|
40
|
+
mock_session: AsyncMock,
|
|
41
|
+
) -> GitSourceFactory:
|
|
42
|
+
"""Create a GitSourceFactory instance with mocked dependencies."""
|
|
43
|
+
return GitSourceFactory(
|
|
44
|
+
repository=mock_repository,
|
|
45
|
+
working_copy=mock_working_copy,
|
|
46
|
+
session=mock_session,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_url_normalization_with_pat_should_sanitize_credentials(
|
|
52
|
+
factory: GitSourceFactory, tmp_path: Path
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Test that URLs with personal access tokens are properly sanitized."""
|
|
55
|
+
# URLs with PATs that should be sanitized
|
|
56
|
+
pat_urls = [
|
|
57
|
+
"https://phil:7lKCobJPAY1ekOS5kxxxxxxxx@dev.azure.com/winderai/private-test/_git/private-test",
|
|
58
|
+
"https://winderai@dev.azure.com/winderai/private-test/_git/private-test",
|
|
59
|
+
"https://username:token123@github.com/username/repo.git",
|
|
60
|
+
"https://user:pass@gitlab.com/user/repo.git",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
expected_sanitized = [
|
|
64
|
+
"https://dev.azure.com/winderai/private-test/_git/private-test",
|
|
65
|
+
"https://dev.azure.com/winderai/private-test/_git/private-test",
|
|
66
|
+
"https://github.com/username/repo.git",
|
|
67
|
+
"https://gitlab.com/user/repo.git",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for i, pat_url in enumerate(pat_urls):
|
|
71
|
+
# Mock the repository to return None (source doesn't exist)
|
|
72
|
+
factory.repository.get_by_uri = AsyncMock(return_value=None)
|
|
73
|
+
|
|
74
|
+
# Mock the working copy to return a temporary path
|
|
75
|
+
temp_clone_path = tmp_path / f"clone_{i}"
|
|
76
|
+
temp_clone_path.mkdir()
|
|
77
|
+
factory.working_copy.prepare = AsyncMock(return_value=temp_clone_path)
|
|
78
|
+
|
|
79
|
+
# Mock the source creation - return a real Source object with the URI that was passed to save
|
|
80
|
+
def mock_save(source_arg):
|
|
81
|
+
# Return the actual Source object that was passed in
|
|
82
|
+
return source_arg
|
|
83
|
+
|
|
84
|
+
factory.repository.save = AsyncMock(side_effect=mock_save)
|
|
85
|
+
|
|
86
|
+
# Create a temporary git repo to simulate the clone
|
|
87
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
88
|
+
repo = git.Repo.init(temp_dir)
|
|
89
|
+
|
|
90
|
+
# Mock the remote URL to return the PAT URL (this simulates the current bug)
|
|
91
|
+
with patch.object(repo, "remote") as mock_remote:
|
|
92
|
+
mock_remote.return_value.url = pat_url
|
|
93
|
+
|
|
94
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
95
|
+
with patch("git.Repo") as mock_git_repo:
|
|
96
|
+
mock_git_repo.return_value = repo
|
|
97
|
+
|
|
98
|
+
# Call the create method
|
|
99
|
+
result = await factory.create(pat_url)
|
|
100
|
+
|
|
101
|
+
# Verify that the URL was sanitized by checking what was passed to repository.save
|
|
102
|
+
assert result.uri == expected_sanitized[i], (
|
|
103
|
+
f"URL should be sanitized: {pat_url} -> {expected_sanitized[i]}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Also verify that repository.save was called with the correct URI
|
|
107
|
+
save_call_args = factory.repository.save.call_args
|
|
108
|
+
assert save_call_args is not None, (
|
|
109
|
+
"repository.save should have been called"
|
|
110
|
+
)
|
|
111
|
+
saved_source = save_call_args[0][0] # First positional argument
|
|
112
|
+
assert saved_source.uri == expected_sanitized[i], (
|
|
113
|
+
f"repository.save was called with wrong URI: {saved_source.uri}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_url_normalization_without_credentials_should_remain_unchanged(
|
|
119
|
+
factory: GitSourceFactory, tmp_path: Path
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Test that URLs without credentials remain unchanged."""
|
|
122
|
+
clean_urls = [
|
|
123
|
+
"https://github.com/username/repo.git",
|
|
124
|
+
"https://dev.azure.com/winderai/public-test/_git/public-test",
|
|
125
|
+
"git@github.com:username/repo.git",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
for i, clean_url in enumerate(clean_urls):
|
|
129
|
+
# Mock the repository to return None (source doesn't exist)
|
|
130
|
+
factory.repository.get_by_uri = AsyncMock(return_value=None)
|
|
131
|
+
|
|
132
|
+
# Mock the working copy to return a temporary path
|
|
133
|
+
temp_clone_path = tmp_path / f"clone_{i}"
|
|
134
|
+
temp_clone_path.mkdir()
|
|
135
|
+
factory.working_copy.prepare = AsyncMock(return_value=temp_clone_path)
|
|
136
|
+
|
|
137
|
+
# Mock the source creation - return a real Source object with the URI that was passed to save
|
|
138
|
+
def mock_save(source_arg):
|
|
139
|
+
# Return the actual Source object that was passed in
|
|
140
|
+
return source_arg
|
|
141
|
+
|
|
142
|
+
factory.repository.save = AsyncMock(side_effect=mock_save)
|
|
143
|
+
|
|
144
|
+
# Create a temporary git repo to simulate the clone
|
|
145
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
146
|
+
repo = git.Repo.init(temp_dir)
|
|
147
|
+
|
|
148
|
+
# Mock the remote URL to return the clean URL
|
|
149
|
+
with patch.object(repo, "remote") as mock_remote:
|
|
150
|
+
mock_remote.return_value.url = clean_url
|
|
151
|
+
|
|
152
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
153
|
+
with patch("git.Repo") as mock_git_repo:
|
|
154
|
+
mock_git_repo.return_value = repo
|
|
155
|
+
|
|
156
|
+
# Call the create method
|
|
157
|
+
result = await factory.create(clean_url)
|
|
158
|
+
|
|
159
|
+
# Verify that the URL remains unchanged
|
|
160
|
+
assert result.uri == clean_url
|
|
161
|
+
|
|
162
|
+
# Also verify that repository.save was called with the correct URI
|
|
163
|
+
save_call_args = factory.repository.save.call_args
|
|
164
|
+
assert save_call_args is not None, (
|
|
165
|
+
"repository.save should have been called"
|
|
166
|
+
)
|
|
167
|
+
saved_source = save_call_args[0][0] # First positional argument
|
|
168
|
+
assert saved_source.uri == clean_url, (
|
|
169
|
+
f"repository.save was called with wrong URI: {saved_source.uri}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_url_normalization_ssh_urls_should_remain_unchanged(
|
|
175
|
+
factory: GitSourceFactory, tmp_path: Path
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Test that SSH URLs remain unchanged."""
|
|
178
|
+
ssh_urls = [
|
|
179
|
+
"git@github.com:username/repo.git",
|
|
180
|
+
"ssh://git@github.com:2222/username/repo.git",
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
for i, ssh_url in enumerate(ssh_urls):
|
|
184
|
+
# Mock the repository to return None (source doesn't exist)
|
|
185
|
+
factory.repository.get_by_uri = AsyncMock(return_value=None)
|
|
186
|
+
|
|
187
|
+
# Mock the working copy to return a temporary path
|
|
188
|
+
temp_clone_path = tmp_path / f"clone_{i}"
|
|
189
|
+
temp_clone_path.mkdir()
|
|
190
|
+
factory.working_copy.prepare = AsyncMock(return_value=temp_clone_path)
|
|
191
|
+
|
|
192
|
+
# Mock the source creation - return a real Source object with the URI that was passed to save
|
|
193
|
+
def mock_save(source_arg):
|
|
194
|
+
# Return the actual Source object that was passed in
|
|
195
|
+
return source_arg
|
|
196
|
+
|
|
197
|
+
factory.repository.save = AsyncMock(side_effect=mock_save)
|
|
198
|
+
|
|
199
|
+
# Create a temporary git repo to simulate the clone
|
|
200
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
201
|
+
repo = git.Repo.init(temp_dir)
|
|
202
|
+
|
|
203
|
+
# Mock the remote URL to return the SSH URL
|
|
204
|
+
with patch.object(repo, "remote") as mock_remote:
|
|
205
|
+
mock_remote.return_value.url = ssh_url
|
|
206
|
+
|
|
207
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
208
|
+
with patch("git.Repo") as mock_git_repo:
|
|
209
|
+
mock_git_repo.return_value = repo
|
|
210
|
+
|
|
211
|
+
# Call the create method
|
|
212
|
+
result = await factory.create(ssh_url)
|
|
213
|
+
|
|
214
|
+
# Verify that the SSH URL remains unchanged
|
|
215
|
+
assert result.uri == ssh_url
|
|
216
|
+
|
|
217
|
+
# Also verify that repository.save was called with the correct URI
|
|
218
|
+
save_call_args = factory.repository.save.call_args
|
|
219
|
+
assert save_call_args is not None, (
|
|
220
|
+
"repository.save should have been called"
|
|
221
|
+
)
|
|
222
|
+
saved_source = save_call_args[0][0] # First positional argument
|
|
223
|
+
assert saved_source.uri == ssh_url, (
|
|
224
|
+
f"repository.save was called with wrong URI: {saved_source.uri}"
|
|
225
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Tests for the git working copy provider module."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import git
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from kodit.infrastructure.cloning.git.working_copy import GitWorkingCopyProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def working_copy(tmp_path: Path) -> GitWorkingCopyProvider:
|
|
14
|
+
"""Create a GitWorkingCopyProvider instance."""
|
|
15
|
+
return GitWorkingCopyProvider(tmp_path)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_prepare_should_not_leak_credentials_in_directory_name(
|
|
20
|
+
working_copy: GitWorkingCopyProvider, tmp_path: Path
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Test that directory names don't contain sensitive credentials."""
|
|
23
|
+
# URLs with PATs that should not appear in directory names
|
|
24
|
+
pat_urls = [
|
|
25
|
+
"https://phil:7lKCobJPAY1ekOS5kxxxxxxxx@dev.azure.com/winderai/private-test/_git/private-test",
|
|
26
|
+
"https://winderai@dev.azure.com/winderai/private-test/_git/private-test",
|
|
27
|
+
"https://username:token123@github.com/username/repo.git",
|
|
28
|
+
"https://user:pass@gitlab.com/user/repo.git",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
expected_safe_directories = [
|
|
32
|
+
"https___dev.azure.com_winderai_private-test__git_private-test",
|
|
33
|
+
"https___dev.azure.com_winderai_private-test__git_private-test",
|
|
34
|
+
"https___github.com_username_repo.git",
|
|
35
|
+
"https___gitlab.com_user_repo.git",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for i, pat_url in enumerate(pat_urls):
|
|
39
|
+
# Mock git.Repo.clone_from to avoid actual cloning
|
|
40
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
41
|
+
# Call the prepare method
|
|
42
|
+
result_path = await working_copy.prepare(pat_url)
|
|
43
|
+
|
|
44
|
+
# Verify that the directory name doesn't contain credentials
|
|
45
|
+
directory_name = result_path.name
|
|
46
|
+
assert directory_name == expected_safe_directories[i], (
|
|
47
|
+
f"Directory name should not contain credentials: {directory_name}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Verify that the directory name doesn't contain the PAT/token
|
|
51
|
+
assert "7lKCobJPAY1ekOS5kxxxxxxxx" not in directory_name, (
|
|
52
|
+
f"Directory name contains PAT: {directory_name}"
|
|
53
|
+
)
|
|
54
|
+
assert "token123" not in directory_name, (
|
|
55
|
+
f"Directory name contains token: {directory_name}"
|
|
56
|
+
)
|
|
57
|
+
assert "pass" not in directory_name, (
|
|
58
|
+
f"Directory name contains password: {directory_name}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Verify that the directory was created
|
|
62
|
+
assert result_path.exists()
|
|
63
|
+
assert result_path.is_dir()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_prepare_clean_urls_should_work_normally(
|
|
68
|
+
working_copy: GitWorkingCopyProvider, tmp_path: Path
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Test that clean URLs work normally without any issues."""
|
|
71
|
+
clean_urls = [
|
|
72
|
+
"https://github.com/username/repo.git",
|
|
73
|
+
"https://dev.azure.com/winderai/public-test/_git/public-test",
|
|
74
|
+
"git@github.com:username/repo.git",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
expected_directories = [
|
|
78
|
+
"https___github.com_username_repo.git",
|
|
79
|
+
"https___dev.azure.com_winderai_public-test__git_public-test",
|
|
80
|
+
"git@github.com_username_repo.git",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
for i, clean_url in enumerate(clean_urls):
|
|
84
|
+
# Mock git.Repo.clone_from to avoid actual cloning
|
|
85
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
86
|
+
# Call the prepare method
|
|
87
|
+
result_path = await working_copy.prepare(clean_url)
|
|
88
|
+
|
|
89
|
+
# Verify that the directory name is as expected
|
|
90
|
+
directory_name = result_path.name
|
|
91
|
+
assert directory_name == expected_directories[i], (
|
|
92
|
+
f"Directory name should match expected: {directory_name}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Verify that the directory was created
|
|
96
|
+
assert result_path.exists()
|
|
97
|
+
assert result_path.is_dir()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_prepare_ssh_urls_should_work_normally(
|
|
102
|
+
working_copy: GitWorkingCopyProvider, tmp_path: Path
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Test that SSH URLs work normally."""
|
|
105
|
+
ssh_urls = [
|
|
106
|
+
"git@github.com:username/repo.git",
|
|
107
|
+
"ssh://git@github.com:2222/username/repo.git",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
expected_directories = [
|
|
111
|
+
"git@github.com_username_repo.git",
|
|
112
|
+
"ssh___git@github.com_2222_username_repo.git",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
for i, ssh_url in enumerate(ssh_urls):
|
|
116
|
+
# Mock git.Repo.clone_from to avoid actual cloning
|
|
117
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
118
|
+
# Call the prepare method
|
|
119
|
+
result_path = await working_copy.prepare(ssh_url)
|
|
120
|
+
|
|
121
|
+
# Verify that the directory name is as expected
|
|
122
|
+
directory_name = result_path.name
|
|
123
|
+
assert directory_name == expected_directories[i], (
|
|
124
|
+
f"Directory name should match expected: {directory_name}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Verify that the directory was created
|
|
128
|
+
assert result_path.exists()
|
|
129
|
+
assert result_path.is_dir()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.asyncio
|
|
133
|
+
async def test_prepare_handles_clone_errors_gracefully(
|
|
134
|
+
working_copy: GitWorkingCopyProvider, tmp_path: Path
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Test that clone errors are handled gracefully."""
|
|
137
|
+
url = "https://github.com/username/repo.git"
|
|
138
|
+
|
|
139
|
+
# Mock git.Repo.clone_from to raise an error
|
|
140
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
141
|
+
mock_clone.side_effect = git.GitCommandError(
|
|
142
|
+
"git", "clone", "Repository not found"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Should raise ValueError for clone errors
|
|
146
|
+
with pytest.raises(ValueError, match="Failed to clone repository"):
|
|
147
|
+
await working_copy.prepare(url)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_prepare_handles_already_exists_error(
|
|
152
|
+
working_copy: GitWorkingCopyProvider, tmp_path: Path
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Test that 'already exists' errors are handled gracefully."""
|
|
155
|
+
url = "https://github.com/username/repo.git"
|
|
156
|
+
|
|
157
|
+
# Mock git.Repo.clone_from to raise an "already exists" error
|
|
158
|
+
with patch("git.Repo.clone_from") as mock_clone:
|
|
159
|
+
mock_clone.side_effect = git.GitCommandError(
|
|
160
|
+
"git", "clone", "already exists and is not an empty directory"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Should not raise an error for "already exists"
|
|
164
|
+
result_path = await working_copy.prepare(url)
|
|
165
|
+
|
|
166
|
+
# Verify that the directory was created
|
|
167
|
+
assert result_path.exists()
|
|
168
|
+
assert result_path.is_dir()
|