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.

Files changed (193) hide show
  1. {kodit-0.2.6 → kodit-0.2.7}/PKG-INFO +1 -1
  2. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/_version.py +2 -2
  3. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/git/factory.py +12 -6
  4. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/git/working_copy.py +12 -4
  5. kodit-0.2.7/src/kodit/infrastructure/git/git_utils.py +87 -0
  6. kodit-0.2.7/tests/kodit/infrastructure/cloning/git/factory_test.py +225 -0
  7. kodit-0.2.7/tests/kodit/infrastructure/cloning/git/working_copy_test.py +168 -0
  8. kodit-0.2.7/tests/kodit/infrastructure/git/test_git_utils.py +127 -0
  9. kodit-0.2.6/src/kodit/infrastructure/git/git_utils.py +0 -24
  10. {kodit-0.2.6 → kodit-0.2.7}/.cursor/rules/kodit.mdc +0 -0
  11. {kodit-0.2.6 → kodit-0.2.7}/.dockerignore +0 -0
  12. {kodit-0.2.6 → kodit-0.2.7}/.github/CODE_OF_CONDUCT.md +0 -0
  13. {kodit-0.2.6 → kodit-0.2.7}/.github/CONTRIBUTING.md +0 -0
  14. {kodit-0.2.6 → kodit-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {kodit-0.2.6 → kodit-0.2.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {kodit-0.2.6 → kodit-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {kodit-0.2.6 → kodit-0.2.7}/.github/dependabot.yml +0 -0
  18. {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/docker.yaml +0 -0
  19. {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/docs.yaml +0 -0
  20. {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/pull_request.yaml +0 -0
  21. {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/pypi-test.yaml +0 -0
  22. {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/pypi.yaml +0 -0
  23. {kodit-0.2.6 → kodit-0.2.7}/.github/workflows/test.yaml +0 -0
  24. {kodit-0.2.6 → kodit-0.2.7}/.gitignore +0 -0
  25. {kodit-0.2.6 → kodit-0.2.7}/.python-version +0 -0
  26. {kodit-0.2.6 → kodit-0.2.7}/.vscode/launch.json +0 -0
  27. {kodit-0.2.6 → kodit-0.2.7}/.vscode/settings.json +0 -0
  28. {kodit-0.2.6 → kodit-0.2.7}/Dockerfile +0 -0
  29. {kodit-0.2.6 → kodit-0.2.7}/LICENSE +0 -0
  30. {kodit-0.2.6 → kodit-0.2.7}/README.md +0 -0
  31. {kodit-0.2.6 → kodit-0.2.7}/alembic.ini +0 -0
  32. {kodit-0.2.6 → kodit-0.2.7}/docs/_index.md +0 -0
  33. {kodit-0.2.6 → kodit-0.2.7}/docs/demos/_index.md +0 -0
  34. {kodit-0.2.6 → kodit-0.2.7}/docs/demos/go-simple-microservice/index.md +0 -0
  35. {kodit-0.2.6 → kodit-0.2.7}/docs/demos/knock-knock-auth/index.md +0 -0
  36. {kodit-0.2.6 → kodit-0.2.7}/docs/developer/index.md +0 -0
  37. {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/_index.md +0 -0
  38. {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/installation/index.md +0 -0
  39. {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/integration/index.md +0 -0
  40. {kodit-0.2.6 → kodit-0.2.7}/docs/getting-started/quick-start/index.md +0 -0
  41. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/_index.md +0 -0
  42. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/configuration/index.md +0 -0
  43. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/deployment/docker-compose.yaml +0 -0
  44. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/deployment/index.md +0 -0
  45. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/deployment/kubernetes.yaml +0 -0
  46. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/indexing/index.md +0 -0
  47. {kodit-0.2.6 → kodit-0.2.7}/docs/reference/telemetry/index.md +0 -0
  48. {kodit-0.2.6 → kodit-0.2.7}/pyproject.toml +0 -0
  49. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/.gitignore +0 -0
  50. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/__init__.py +0 -0
  51. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/app.py +0 -0
  52. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/__init__.py +0 -0
  53. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/commands/__init__.py +0 -0
  54. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/commands/snippet_commands.py +0 -0
  55. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/services/__init__.py +0 -0
  56. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/services/indexing_application_service.py +0 -0
  57. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/application/services/snippet_application_service.py +0 -0
  58. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/cli.py +0 -0
  59. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/config.py +0 -0
  60. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/database.py +0 -0
  61. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/__init__.py +0 -0
  62. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/entities.py +0 -0
  63. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/enums.py +0 -0
  64. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/errors.py +0 -0
  65. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/interfaces.py +0 -0
  66. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/repositories.py +0 -0
  67. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/__init__.py +0 -0
  68. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/bm25_service.py +0 -0
  69. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/embedding_service.py +0 -0
  70. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/enrichment_service.py +0 -0
  71. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/ignore_service.py +0 -0
  72. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/indexing_service.py +0 -0
  73. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/snippet_extraction_service.py +0 -0
  74. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/services/source_service.py +0 -0
  75. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/domain/value_objects.py +0 -0
  76. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/__init__.py +0 -0
  77. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/__init__.py +0 -0
  78. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/bm25_factory.py +0 -0
  79. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/local_bm25_repository.py +0 -0
  80. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/bm25/vectorchord_bm25_repository.py +0 -0
  81. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/__init__.py +0 -0
  82. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/folder/__init__.py +0 -0
  83. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/folder/factory.py +0 -0
  84. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/folder/working_copy.py +0 -0
  85. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/git/__init__.py +0 -0
  86. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/cloning/metadata.py +0 -0
  87. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/__init__.py +0 -0
  88. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_factory.py +0 -0
  89. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/__init__.py +0 -0
  90. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/batching.py +0 -0
  91. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/hash_embedding_provider.py +0 -0
  92. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/local_embedding_provider.py +0 -0
  93. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/embedding_providers/openai_embedding_provider.py +0 -0
  94. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/local_vector_search_repository.py +0 -0
  95. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/embedding/vectorchord_vector_search_repository.py +0 -0
  96. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/__init__.py +0 -0
  97. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/enrichment_factory.py +0 -0
  98. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/legacy_enrichment_models.py +0 -0
  99. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/local_enrichment_provider.py +0 -0
  100. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -0
  101. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/enrichment/openai_enrichment_provider.py +0 -0
  102. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/git/__init__.py +0 -0
  103. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ignore/__init__.py +0 -0
  104. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ignore/ignore_pattern_provider.py +0 -0
  105. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/__init__.py +0 -0
  106. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/fusion_service.py +0 -0
  107. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/index_repository.py +0 -0
  108. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/indexing/indexing_factory.py +0 -0
  109. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/__init__.py +0 -0
  110. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/language_detection_service.py +0 -0
  111. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/csharp.scm +0 -0
  112. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/go.scm +0 -0
  113. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/javascript.scm +0 -0
  114. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/python.scm +0 -0
  115. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/languages/typescript.scm +0 -0
  116. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/snippet_extraction_factory.py +0 -0
  117. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/snippet_query_provider.py +0 -0
  118. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/snippet_extraction/tree_sitter_snippet_extractor.py +0 -0
  119. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/__init__.py +0 -0
  120. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/embedding_repository.py +0 -0
  121. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/file_repository.py +0 -0
  122. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/repository.py +0 -0
  123. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/sqlalchemy/snippet_repository.py +0 -0
  124. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ui/__init__.py +0 -0
  125. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ui/progress.py +0 -0
  126. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/infrastructure/ui/spinner.py +0 -0
  127. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/log.py +0 -0
  128. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/mcp.py +0 -0
  129. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/middleware.py +0 -0
  130. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/README +0 -0
  131. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/__init__.py +0 -0
  132. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/env.py +0 -0
  133. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/script.py.mako +0 -0
  134. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/7c3bbc2ab32b_add_embeddings_table.py +0 -0
  135. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/85155663351e_initial.py +0 -0
  136. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/9e53ea8bb3b0_add_authors.py +0 -0
  137. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/__init__.py +0 -0
  138. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/migrations/versions/c3f5137d30f5_index_all_the_things.py +0 -0
  139. {kodit-0.2.6 → kodit-0.2.7}/src/kodit/reporting.py +0 -0
  140. {kodit-0.2.6 → kodit-0.2.7}/tests/__init__.py +0 -0
  141. {kodit-0.2.6 → kodit-0.2.7}/tests/conftest.py +0 -0
  142. {kodit-0.2.6 → kodit-0.2.7}/tests/docker-smoke.sh +0 -0
  143. {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/cline-prompt-regression-tests/cline_prompt.txt +0 -0
  144. {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/cline-prompt-regression-tests/cline_prompt_test.py +0 -0
  145. {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/embedding.py +0 -0
  146. {kodit-0.2.6 → kodit-0.2.7}/tests/experiments/similarity_test.py +0 -0
  147. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/__init__.py +0 -0
  148. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/application/__init__.py +0 -0
  149. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/application/indexing_application_service_test.py +0 -0
  150. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/application/snippet_application_service_test.py +0 -0
  151. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/cli_test.py +0 -0
  152. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/__init__.py +0 -0
  153. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/bm25_domain_service_test.py +0 -0
  154. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/enrichment_domain_service_test.py +0 -0
  155. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/snippet_extraction_domain_service_test.py +0 -0
  156. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/test_embedding_service.py +0 -0
  157. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/domain/test_models.py +0 -0
  158. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/e2e.py +0 -0
  159. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/__init__.py +0 -0
  160. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/__init__.py +0 -0
  161. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_factory_test.py +0 -0
  162. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_provider/test_hash_embedding_provider.py +0 -0
  163. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_provider/test_local_embedding_provider.py +0 -0
  164. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/embedding_provider/test_openai_embedding_provider.py +0 -0
  165. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_batching.py +0 -0
  166. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_embedding_integration.py +0 -0
  167. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_local_vector_search_repository.py +0 -0
  168. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/embedding/test_vectorchord_vector_search_repository.py +0 -0
  169. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/__init__.py +0 -0
  170. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/__init__.py +0 -0
  171. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/test_local_enrichment_provider.py +0 -0
  172. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/test_null_enrichment_provider.py +0 -0
  173. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/enrichment_provider/test_openai_enrichment_provider.py +0 -0
  174. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/test_enrichment_factory.py +0 -0
  175. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/enrichment/test_enrichment_integration.py +0 -0
  176. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/indexing/__init__.py +0 -0
  177. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/indexing/indexing_repository_test.py +0 -0
  178. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/__init__.py +0 -0
  179. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/csharp.cs +0 -0
  180. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/golang.go +0 -0
  181. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/javascript.js +0 -0
  182. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/knock-knock-server.py +0 -0
  183. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/python.py +0 -0
  184. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/snippets/typescript.tsx +0 -0
  185. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/source/__init__.py +0 -0
  186. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/source/source_service_test.py +0 -0
  187. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/infrastructure/sqlalchemy/test_embedding_repository.py +0 -0
  188. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/log_test.py +0 -0
  189. {kodit-0.2.6 → kodit-0.2.7}/tests/kodit/mcp_test.py +0 -0
  190. {kodit-0.2.6 → kodit-0.2.7}/tests/performance/similarity.py +0 -0
  191. {kodit-0.2.6 → kodit-0.2.7}/tests/smoke.sh +0 -0
  192. {kodit-0.2.6 → kodit-0.2.7}/tests/vectorchord-smoke.sh +0 -0
  193. {kodit-0.2.6 → kodit-0.2.7}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kodit
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Code indexing for better AI code generation
5
5
  Project-URL: Homepage, https://docs.helixml.tech/kodit/
6
6
  Project-URL: Documentation, https://docs.helixml.tech/kodit/
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.6'
21
- __version_tuple__ = version_tuple = (0, 2, 6)
20
+ __version__ = version = '0.2.7'
21
+ __version_tuple__ = version_tuple = (0, 2, 7)
@@ -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
- self.log.debug("Normalising git uri", uri=uri)
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=uri)
57
- source = await self.repository.get_by_uri(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=uri, clone_path=str(clone_path))
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=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
- # Create a unique directory name for the clone
20
- clone_path = self.clone_dir / uri.replace("/", "_").replace(":", "_")
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("Cloning repository", uri=uri, clone_path=str(clone_path))
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=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()