kodit 0.4.3__py3-none-any.whl → 0.5.1__py3-none-any.whl
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/_version.py +2 -2
- kodit/app.py +51 -23
- kodit/application/factories/reporting_factory.py +6 -2
- kodit/application/factories/server_factory.py +353 -0
- kodit/application/services/code_search_application_service.py +144 -0
- kodit/application/services/commit_indexing_application_service.py +700 -0
- kodit/application/services/indexing_worker_service.py +13 -44
- kodit/application/services/queue_service.py +24 -3
- kodit/application/services/reporting.py +0 -2
- kodit/application/services/sync_scheduler.py +15 -31
- kodit/cli.py +2 -753
- kodit/cli_utils.py +2 -9
- kodit/config.py +4 -97
- kodit/database.py +38 -1
- kodit/domain/enrichments/__init__.py +1 -0
- kodit/domain/enrichments/architecture/__init__.py +1 -0
- kodit/domain/enrichments/architecture/architecture.py +20 -0
- kodit/domain/enrichments/architecture/physical/__init__.py +1 -0
- kodit/domain/enrichments/architecture/physical/discovery_notes.py +14 -0
- kodit/domain/enrichments/architecture/physical/formatter.py +11 -0
- kodit/domain/enrichments/architecture/physical/physical.py +17 -0
- kodit/domain/enrichments/development/__init__.py +1 -0
- kodit/domain/enrichments/development/development.py +18 -0
- kodit/domain/enrichments/development/snippet/__init__.py +1 -0
- kodit/domain/enrichments/development/snippet/snippet.py +21 -0
- kodit/domain/enrichments/enricher.py +17 -0
- kodit/domain/enrichments/enrichment.py +39 -0
- kodit/domain/enrichments/request.py +12 -0
- kodit/domain/enrichments/response.py +11 -0
- kodit/domain/enrichments/usage/__init__.py +1 -0
- kodit/domain/enrichments/usage/api_docs.py +19 -0
- kodit/domain/enrichments/usage/usage.py +18 -0
- kodit/domain/{entities.py → entities/__init__.py} +50 -195
- kodit/domain/entities/git.py +190 -0
- kodit/domain/factories/__init__.py +1 -0
- kodit/domain/factories/git_repo_factory.py +76 -0
- kodit/domain/protocols.py +264 -64
- kodit/domain/services/bm25_service.py +5 -1
- kodit/domain/services/embedding_service.py +3 -0
- kodit/domain/services/enrichment_service.py +9 -30
- kodit/domain/services/git_repository_service.py +429 -0
- kodit/domain/services/git_service.py +300 -0
- kodit/domain/services/physical_architecture_service.py +182 -0
- kodit/domain/services/task_status_query_service.py +2 -2
- kodit/domain/value_objects.py +87 -135
- kodit/infrastructure/api/client/__init__.py +0 -2
- kodit/infrastructure/api/v1/__init__.py +0 -4
- kodit/infrastructure/api/v1/dependencies.py +92 -46
- kodit/infrastructure/api/v1/routers/__init__.py +0 -6
- kodit/infrastructure/api/v1/routers/commits.py +352 -0
- kodit/infrastructure/api/v1/routers/queue.py +2 -2
- kodit/infrastructure/api/v1/routers/repositories.py +282 -0
- kodit/infrastructure/api/v1/routers/search.py +31 -14
- kodit/infrastructure/api/v1/schemas/__init__.py +0 -24
- kodit/infrastructure/api/v1/schemas/commit.py +96 -0
- kodit/infrastructure/api/v1/schemas/context.py +2 -0
- kodit/infrastructure/api/v1/schemas/enrichment.py +29 -0
- kodit/infrastructure/api/v1/schemas/repository.py +128 -0
- kodit/infrastructure/api/v1/schemas/search.py +12 -9
- kodit/infrastructure/api/v1/schemas/snippet.py +58 -0
- kodit/infrastructure/api/v1/schemas/tag.py +31 -0
- kodit/infrastructure/api/v1/schemas/task_status.py +2 -0
- kodit/infrastructure/bm25/local_bm25_repository.py +16 -4
- kodit/infrastructure/bm25/vectorchord_bm25_repository.py +68 -52
- kodit/infrastructure/cloning/git/git_python_adaptor.py +534 -0
- kodit/infrastructure/cloning/git/working_copy.py +1 -1
- kodit/infrastructure/embedding/embedding_factory.py +3 -2
- kodit/infrastructure/embedding/local_vector_search_repository.py +1 -1
- kodit/infrastructure/embedding/vectorchord_vector_search_repository.py +111 -84
- kodit/infrastructure/enricher/__init__.py +1 -0
- kodit/infrastructure/enricher/enricher_factory.py +53 -0
- kodit/infrastructure/{enrichment/litellm_enrichment_provider.py → enricher/litellm_enricher.py} +36 -56
- kodit/infrastructure/{enrichment/local_enrichment_provider.py → enricher/local_enricher.py} +19 -24
- kodit/infrastructure/enricher/null_enricher.py +36 -0
- kodit/infrastructure/indexing/fusion_service.py +1 -1
- kodit/infrastructure/mappers/enrichment_mapper.py +83 -0
- kodit/infrastructure/mappers/git_mapper.py +193 -0
- kodit/infrastructure/mappers/snippet_mapper.py +104 -0
- kodit/infrastructure/mappers/task_mapper.py +5 -44
- kodit/infrastructure/physical_architecture/__init__.py +1 -0
- kodit/infrastructure/physical_architecture/detectors/__init__.py +1 -0
- kodit/infrastructure/physical_architecture/detectors/docker_compose_detector.py +336 -0
- kodit/infrastructure/physical_architecture/formatters/__init__.py +1 -0
- kodit/infrastructure/physical_architecture/formatters/narrative_formatter.py +149 -0
- kodit/infrastructure/reporting/log_progress.py +8 -5
- kodit/infrastructure/reporting/telemetry_progress.py +21 -0
- kodit/infrastructure/slicing/api_doc_extractor.py +836 -0
- kodit/infrastructure/slicing/ast_analyzer.py +1128 -0
- kodit/infrastructure/slicing/slicer.py +87 -421
- kodit/infrastructure/sqlalchemy/embedding_repository.py +43 -23
- kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +118 -0
- kodit/infrastructure/sqlalchemy/entities.py +402 -158
- kodit/infrastructure/sqlalchemy/git_branch_repository.py +274 -0
- kodit/infrastructure/sqlalchemy/git_commit_repository.py +346 -0
- kodit/infrastructure/sqlalchemy/git_repository.py +262 -0
- kodit/infrastructure/sqlalchemy/git_tag_repository.py +268 -0
- kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +479 -0
- kodit/infrastructure/sqlalchemy/task_repository.py +29 -23
- kodit/infrastructure/sqlalchemy/task_status_repository.py +24 -12
- kodit/infrastructure/sqlalchemy/unit_of_work.py +10 -14
- kodit/mcp.py +12 -30
- kodit/migrations/env.py +1 -0
- kodit/migrations/versions/04b80f802e0c_foreign_key_review.py +100 -0
- kodit/migrations/versions/19f8c7faf8b9_add_generic_enrichment_type.py +260 -0
- kodit/migrations/versions/7f15f878c3a1_add_new_git_entities.py +690 -0
- kodit/migrations/versions/f9e5ef5e688f_add_git_commits_number.py +43 -0
- kodit/py.typed +0 -0
- kodit/utils/dump_config.py +361 -0
- kodit/utils/dump_openapi.py +6 -4
- kodit/utils/path_utils.py +29 -0
- {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/METADATA +3 -3
- kodit-0.5.1.dist-info/RECORD +168 -0
- kodit/application/factories/code_indexing_factory.py +0 -195
- kodit/application/services/auto_indexing_service.py +0 -99
- kodit/application/services/code_indexing_application_service.py +0 -410
- kodit/domain/services/index_query_service.py +0 -70
- kodit/domain/services/index_service.py +0 -269
- kodit/infrastructure/api/client/index_client.py +0 -57
- kodit/infrastructure/api/v1/routers/indexes.py +0 -164
- kodit/infrastructure/api/v1/schemas/index.py +0 -101
- kodit/infrastructure/bm25/bm25_factory.py +0 -28
- kodit/infrastructure/cloning/__init__.py +0 -1
- kodit/infrastructure/cloning/metadata.py +0 -98
- kodit/infrastructure/enrichment/__init__.py +0 -1
- kodit/infrastructure/enrichment/enrichment_factory.py +0 -52
- kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -19
- kodit/infrastructure/mappers/index_mapper.py +0 -345
- kodit/infrastructure/reporting/tdqm_progress.py +0 -38
- kodit/infrastructure/slicing/language_detection_service.py +0 -18
- kodit/infrastructure/sqlalchemy/index_repository.py +0 -646
- kodit-0.4.3.dist-info/RECORD +0 -125
- /kodit/infrastructure/{enrichment → enricher}/utils.py +0 -0
- {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/WHEEL +0 -0
- {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/entry_points.txt +0 -0
- {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Commit management router for the REST API."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
6
|
+
|
|
7
|
+
from kodit.infrastructure.api.middleware.auth import api_key_auth
|
|
8
|
+
from kodit.infrastructure.api.v1.dependencies import (
|
|
9
|
+
GitCommitRepositoryDep,
|
|
10
|
+
ServerFactoryDep,
|
|
11
|
+
)
|
|
12
|
+
from kodit.infrastructure.api.v1.schemas.commit import (
|
|
13
|
+
CommitAttributes,
|
|
14
|
+
CommitData,
|
|
15
|
+
CommitListResponse,
|
|
16
|
+
CommitResponse,
|
|
17
|
+
EmbeddingAttributes,
|
|
18
|
+
EmbeddingData,
|
|
19
|
+
EmbeddingListResponse,
|
|
20
|
+
FileAttributes,
|
|
21
|
+
FileData,
|
|
22
|
+
FileListResponse,
|
|
23
|
+
FileResponse,
|
|
24
|
+
)
|
|
25
|
+
from kodit.infrastructure.api.v1.schemas.enrichment import (
|
|
26
|
+
EnrichmentAttributes,
|
|
27
|
+
EnrichmentData,
|
|
28
|
+
EnrichmentListResponse,
|
|
29
|
+
)
|
|
30
|
+
from kodit.infrastructure.api.v1.schemas.snippet import (
|
|
31
|
+
EnrichmentSchema,
|
|
32
|
+
GitFileSchema,
|
|
33
|
+
SnippetAttributes,
|
|
34
|
+
SnippetContentSchema,
|
|
35
|
+
SnippetData,
|
|
36
|
+
SnippetListResponse,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
router = APIRouter(
|
|
40
|
+
prefix="/api/v1/repositories",
|
|
41
|
+
tags=["commits"],
|
|
42
|
+
dependencies=[Depends(api_key_auth)],
|
|
43
|
+
responses={
|
|
44
|
+
401: {"description": "Unauthorized"},
|
|
45
|
+
422: {"description": "Invalid request"},
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/{repo_id}/commits", summary="List repository commits")
|
|
51
|
+
async def list_repository_commits(
|
|
52
|
+
repo_id: str, git_commit_repository: GitCommitRepositoryDep
|
|
53
|
+
) -> CommitListResponse:
|
|
54
|
+
"""List all commits for a repository."""
|
|
55
|
+
try:
|
|
56
|
+
# Get all commits for the repository directly from commit repository
|
|
57
|
+
commits = await git_commit_repository.get_by_repo_id(int(repo_id))
|
|
58
|
+
except ValueError as e:
|
|
59
|
+
raise HTTPException(status_code=404, detail="Repository not found") from e
|
|
60
|
+
|
|
61
|
+
return CommitListResponse(
|
|
62
|
+
data=[
|
|
63
|
+
CommitData(
|
|
64
|
+
type="commit",
|
|
65
|
+
id=commit.commit_sha,
|
|
66
|
+
attributes=CommitAttributes(
|
|
67
|
+
commit_sha=commit.commit_sha,
|
|
68
|
+
date=commit.date,
|
|
69
|
+
message=commit.message,
|
|
70
|
+
parent_commit_sha=commit.parent_commit_sha or "",
|
|
71
|
+
author=commit.author,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
for commit in commits
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.get(
|
|
80
|
+
"/{repo_id}/commits/{commit_sha}",
|
|
81
|
+
summary="Get repository commit",
|
|
82
|
+
responses={404: {"description": "Repository or commit not found"}},
|
|
83
|
+
)
|
|
84
|
+
async def get_repository_commit(
|
|
85
|
+
repo_id: str, # noqa: ARG001
|
|
86
|
+
commit_sha: str,
|
|
87
|
+
git_commit_repository: GitCommitRepositoryDep,
|
|
88
|
+
) -> CommitResponse:
|
|
89
|
+
"""Get a specific commit for a repository."""
|
|
90
|
+
try:
|
|
91
|
+
# Get the specific commit directly from commit repository
|
|
92
|
+
commit = await git_commit_repository.get_by_sha(commit_sha)
|
|
93
|
+
except ValueError as e:
|
|
94
|
+
raise HTTPException(status_code=404, detail="Commit not found") from e
|
|
95
|
+
|
|
96
|
+
return CommitResponse(
|
|
97
|
+
data=CommitData(
|
|
98
|
+
type="commit",
|
|
99
|
+
id=commit.commit_sha,
|
|
100
|
+
attributes=CommitAttributes(
|
|
101
|
+
commit_sha=commit.commit_sha,
|
|
102
|
+
date=commit.date,
|
|
103
|
+
message=commit.message,
|
|
104
|
+
parent_commit_sha=commit.parent_commit_sha or "",
|
|
105
|
+
author=commit.author,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.get("/{repo_id}/commits/{commit_sha}/files", summary="List commit files")
|
|
112
|
+
async def list_commit_files(
|
|
113
|
+
repo_id: str, # noqa: ARG001
|
|
114
|
+
commit_sha: str,
|
|
115
|
+
git_commit_repository: GitCommitRepositoryDep,
|
|
116
|
+
) -> FileListResponse:
|
|
117
|
+
"""List all files in a specific commit."""
|
|
118
|
+
try:
|
|
119
|
+
# Get the specific commit directly from commit repository
|
|
120
|
+
commit = await git_commit_repository.get_by_sha(commit_sha)
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
raise HTTPException(status_code=404, detail="Commit not found") from e
|
|
123
|
+
|
|
124
|
+
return FileListResponse(
|
|
125
|
+
data=[
|
|
126
|
+
FileData(
|
|
127
|
+
type="file",
|
|
128
|
+
id=file.blob_sha,
|
|
129
|
+
attributes=FileAttributes(
|
|
130
|
+
blob_sha=file.blob_sha,
|
|
131
|
+
path=file.path,
|
|
132
|
+
mime_type=file.mime_type,
|
|
133
|
+
size=file.size,
|
|
134
|
+
extension=file.extension,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
for file in commit.files
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.get(
|
|
143
|
+
"/{repo_id}/commits/{commit_sha}/files/{blob_sha}",
|
|
144
|
+
summary="Get commit file",
|
|
145
|
+
responses={404: {"description": "Repository, commit or file not found"}},
|
|
146
|
+
)
|
|
147
|
+
async def get_commit_file(
|
|
148
|
+
repo_id: str, # noqa: ARG001
|
|
149
|
+
commit_sha: str,
|
|
150
|
+
blob_sha: str,
|
|
151
|
+
git_commit_repository: GitCommitRepositoryDep,
|
|
152
|
+
) -> FileResponse:
|
|
153
|
+
"""Get a specific file from a commit."""
|
|
154
|
+
try:
|
|
155
|
+
# Get the specific commit directly from commit repository
|
|
156
|
+
commit = await git_commit_repository.get_by_sha(commit_sha)
|
|
157
|
+
except ValueError as e:
|
|
158
|
+
raise HTTPException(status_code=404, detail="Commit not found") from e
|
|
159
|
+
|
|
160
|
+
# Find the specific file
|
|
161
|
+
file = next((f for f in commit.files if f.blob_sha == blob_sha), None)
|
|
162
|
+
if not file:
|
|
163
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
164
|
+
|
|
165
|
+
return FileResponse(
|
|
166
|
+
data=FileData(
|
|
167
|
+
type="file",
|
|
168
|
+
id=file.blob_sha,
|
|
169
|
+
attributes=FileAttributes(
|
|
170
|
+
blob_sha=file.blob_sha,
|
|
171
|
+
path=file.path,
|
|
172
|
+
mime_type=file.mime_type,
|
|
173
|
+
size=file.size,
|
|
174
|
+
extension=file.extension,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.get(
|
|
181
|
+
"/{repo_id}/commits/{commit_sha}/snippets",
|
|
182
|
+
summary="List commit snippets",
|
|
183
|
+
responses={404: {"description": "Repository or commit not found"}},
|
|
184
|
+
)
|
|
185
|
+
async def list_commit_snippets(
|
|
186
|
+
repo_id: str,
|
|
187
|
+
commit_sha: str,
|
|
188
|
+
server_factory: ServerFactoryDep,
|
|
189
|
+
) -> SnippetListResponse:
|
|
190
|
+
"""List all snippets in a specific commit."""
|
|
191
|
+
_ = repo_id # Required by FastAPI route path but not used in function
|
|
192
|
+
snippet_repository = server_factory.snippet_v2_repository()
|
|
193
|
+
snippets = await snippet_repository.get_snippets_for_commit(commit_sha)
|
|
194
|
+
|
|
195
|
+
return SnippetListResponse(
|
|
196
|
+
data=[
|
|
197
|
+
SnippetData(
|
|
198
|
+
type="snippet",
|
|
199
|
+
id=snippet.sha,
|
|
200
|
+
attributes=SnippetAttributes(
|
|
201
|
+
created_at=snippet.created_at,
|
|
202
|
+
updated_at=snippet.updated_at,
|
|
203
|
+
derives_from=[
|
|
204
|
+
GitFileSchema(
|
|
205
|
+
blob_sha=file.blob_sha,
|
|
206
|
+
path=file.path,
|
|
207
|
+
mime_type=file.mime_type,
|
|
208
|
+
size=file.size,
|
|
209
|
+
)
|
|
210
|
+
for file in snippet.derives_from
|
|
211
|
+
],
|
|
212
|
+
content=SnippetContentSchema(
|
|
213
|
+
value=snippet.content,
|
|
214
|
+
language=snippet.extension,
|
|
215
|
+
),
|
|
216
|
+
enrichments=[
|
|
217
|
+
EnrichmentSchema(
|
|
218
|
+
type=enrichment.type.value,
|
|
219
|
+
content=enrichment.content,
|
|
220
|
+
)
|
|
221
|
+
for enrichment in snippet.enrichments
|
|
222
|
+
],
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
for snippet in snippets
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# TODO(Phil): This doesn't return vectorchord embeddings properly because it's
|
|
231
|
+
# implemented in a different repo
|
|
232
|
+
@router.get(
|
|
233
|
+
"/{repo_id}/commits/{commit_sha}/embeddings",
|
|
234
|
+
summary="List commit embeddings",
|
|
235
|
+
responses={404: {"description": "Repository or commit not found"}},
|
|
236
|
+
)
|
|
237
|
+
async def list_commit_embeddings(
|
|
238
|
+
repo_id: str,
|
|
239
|
+
commit_sha: str,
|
|
240
|
+
server_factory: ServerFactoryDep,
|
|
241
|
+
full: Annotated[ # noqa: FBT002
|
|
242
|
+
bool,
|
|
243
|
+
Query(
|
|
244
|
+
description="If true, return full vectors. If false, return first 5 values."
|
|
245
|
+
),
|
|
246
|
+
] = False,
|
|
247
|
+
) -> EmbeddingListResponse:
|
|
248
|
+
"""List all embeddings for snippets in a specific commit."""
|
|
249
|
+
_ = repo_id # Required by FastAPI route path but not used in function
|
|
250
|
+
snippet_repository = server_factory.snippet_v2_repository()
|
|
251
|
+
snippets = await snippet_repository.get_snippets_for_commit(commit_sha)
|
|
252
|
+
|
|
253
|
+
if not snippets:
|
|
254
|
+
return EmbeddingListResponse(data=[])
|
|
255
|
+
|
|
256
|
+
# Get snippet SHAs
|
|
257
|
+
snippet_shas = [snippet.sha for snippet in snippets]
|
|
258
|
+
|
|
259
|
+
# Get embeddings for all snippets in the commit
|
|
260
|
+
embedding_repository = server_factory.embedding_repository()
|
|
261
|
+
embeddings = await embedding_repository.get_embeddings_by_snippet_ids(snippet_shas)
|
|
262
|
+
|
|
263
|
+
return EmbeddingListResponse(
|
|
264
|
+
data=[
|
|
265
|
+
EmbeddingData(
|
|
266
|
+
type="embedding",
|
|
267
|
+
id=f"{embedding.snippet_id}_{embedding.type.value}",
|
|
268
|
+
attributes=EmbeddingAttributes(
|
|
269
|
+
snippet_sha=embedding.snippet_id,
|
|
270
|
+
embedding_type=embedding.type.name.lower(),
|
|
271
|
+
embedding=embedding.embedding if full else embedding.embedding[:5],
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
for embedding in embeddings
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@router.get(
|
|
280
|
+
"/{repo_id}/commits/{commit_sha}/enrichments",
|
|
281
|
+
summary="List commit enrichments",
|
|
282
|
+
responses={404: {"description": "Repository or commit not found"}},
|
|
283
|
+
)
|
|
284
|
+
async def list_commit_enrichments(
|
|
285
|
+
repo_id: str, # noqa: ARG001
|
|
286
|
+
commit_sha: str,
|
|
287
|
+
server_factory: ServerFactoryDep,
|
|
288
|
+
) -> EnrichmentListResponse:
|
|
289
|
+
"""List all enrichments for a specific commit."""
|
|
290
|
+
# TODO(Phil): Should use repo too, it's confusing to the user when they specify the
|
|
291
|
+
# wrong commit and another repo. It's like they are seeing results from the other
|
|
292
|
+
# repo.
|
|
293
|
+
enrichment_v2_repository = server_factory.enrichment_v2_repository()
|
|
294
|
+
enrichments = await enrichment_v2_repository.enrichments_for_entity_type(
|
|
295
|
+
entity_type="git_commit",
|
|
296
|
+
entity_ids=[commit_sha],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return EnrichmentListResponse(
|
|
300
|
+
data=[
|
|
301
|
+
EnrichmentData(
|
|
302
|
+
type="enrichment",
|
|
303
|
+
id=str(enrichment.id),
|
|
304
|
+
attributes=EnrichmentAttributes(
|
|
305
|
+
type=enrichment.type,
|
|
306
|
+
subtype=enrichment.subtype,
|
|
307
|
+
content=enrichment.content,
|
|
308
|
+
created_at=enrichment.created_at,
|
|
309
|
+
updated_at=enrichment.updated_at,
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
for enrichment in enrichments
|
|
313
|
+
]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@router.delete(
|
|
318
|
+
"/{repo_id}/commits/{commit_sha}/enrichments",
|
|
319
|
+
summary="Delete all commit enrichments",
|
|
320
|
+
responses={404: {"description": "Commit not found"}},
|
|
321
|
+
status_code=204,
|
|
322
|
+
)
|
|
323
|
+
async def delete_all_commit_enrichments(
|
|
324
|
+
repo_id: str, # noqa: ARG001
|
|
325
|
+
commit_sha: str,
|
|
326
|
+
server_factory: ServerFactoryDep,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Delete all enrichments for a specific commit."""
|
|
329
|
+
enrichment_v2_repository = server_factory.enrichment_v2_repository()
|
|
330
|
+
await enrichment_v2_repository.bulk_delete_enrichments(
|
|
331
|
+
entity_type="git_commit",
|
|
332
|
+
entity_ids=[commit_sha],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@router.delete(
|
|
337
|
+
"/{repo_id}/commits/{commit_sha}/enrichments/{enrichment_id}",
|
|
338
|
+
summary="Delete commit enrichment",
|
|
339
|
+
responses={404: {"description": "Enrichment not found"}},
|
|
340
|
+
status_code=204,
|
|
341
|
+
)
|
|
342
|
+
async def delete_commit_enrichment(
|
|
343
|
+
repo_id: str, # noqa: ARG001
|
|
344
|
+
commit_sha: str, # noqa: ARG001
|
|
345
|
+
enrichment_id: int,
|
|
346
|
+
server_factory: ServerFactoryDep,
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Delete a specific enrichment for a commit."""
|
|
349
|
+
enrichment_v2_repository = server_factory.enrichment_v2_repository()
|
|
350
|
+
deleted = await enrichment_v2_repository.delete_enrichment(enrichment_id)
|
|
351
|
+
if not deleted:
|
|
352
|
+
raise HTTPException(status_code=404, detail="Enrichment not found")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException
|
|
4
4
|
|
|
5
|
-
from kodit.domain.value_objects import
|
|
5
|
+
from kodit.domain.value_objects import TaskOperation
|
|
6
6
|
from kodit.infrastructure.api.middleware.auth import api_key_auth
|
|
7
7
|
from kodit.infrastructure.api.v1.dependencies import QueueServiceDep
|
|
8
8
|
from kodit.infrastructure.api.v1.schemas.queue import (
|
|
@@ -26,7 +26,7 @@ router = APIRouter(
|
|
|
26
26
|
@router.get("")
|
|
27
27
|
async def list_queue_tasks(
|
|
28
28
|
queue_service: QueueServiceDep,
|
|
29
|
-
task_type:
|
|
29
|
+
task_type: TaskOperation | None = None,
|
|
30
30
|
) -> TaskListResponse:
|
|
31
31
|
"""List all tasks in the queue.
|
|
32
32
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Repository management router for the REST API."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
4
|
+
|
|
5
|
+
from kodit.infrastructure.api.middleware.auth import api_key_auth
|
|
6
|
+
from kodit.infrastructure.api.v1.dependencies import (
|
|
7
|
+
CommitIndexingAppServiceDep,
|
|
8
|
+
GitBranchRepositoryDep,
|
|
9
|
+
GitCommitRepositoryDep,
|
|
10
|
+
GitRepositoryDep,
|
|
11
|
+
GitTagRepositoryDep,
|
|
12
|
+
TaskStatusQueryServiceDep,
|
|
13
|
+
)
|
|
14
|
+
from kodit.infrastructure.api.v1.schemas.repository import (
|
|
15
|
+
RepositoryBranchData,
|
|
16
|
+
RepositoryCommitData,
|
|
17
|
+
RepositoryCreateRequest,
|
|
18
|
+
RepositoryData,
|
|
19
|
+
RepositoryDetailsResponse,
|
|
20
|
+
RepositoryListResponse,
|
|
21
|
+
RepositoryResponse,
|
|
22
|
+
)
|
|
23
|
+
from kodit.infrastructure.api.v1.schemas.tag import (
|
|
24
|
+
TagAttributes,
|
|
25
|
+
TagData,
|
|
26
|
+
TagListResponse,
|
|
27
|
+
TagResponse,
|
|
28
|
+
)
|
|
29
|
+
from kodit.infrastructure.api.v1.schemas.task_status import (
|
|
30
|
+
TaskStatusAttributes,
|
|
31
|
+
TaskStatusData,
|
|
32
|
+
TaskStatusListResponse,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
router = APIRouter(
|
|
36
|
+
prefix="/api/v1/repositories",
|
|
37
|
+
tags=["repositories"],
|
|
38
|
+
dependencies=[Depends(api_key_auth)],
|
|
39
|
+
responses={
|
|
40
|
+
401: {"description": "Unauthorized"},
|
|
41
|
+
422: {"description": "Invalid request"},
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _raise_not_found_error(detail: str) -> None:
|
|
47
|
+
"""Raise repository not found error."""
|
|
48
|
+
raise HTTPException(status_code=404, detail=detail)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get("", summary="List repositories")
|
|
52
|
+
async def list_repositories(
|
|
53
|
+
git_repository: GitRepositoryDep,
|
|
54
|
+
) -> RepositoryListResponse:
|
|
55
|
+
"""List all cloned repositories."""
|
|
56
|
+
repos = await git_repository.get_all()
|
|
57
|
+
return RepositoryListResponse(
|
|
58
|
+
data=[RepositoryData.from_git_repo(repo) for repo in repos]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("", status_code=201, summary="Create repository")
|
|
63
|
+
async def create_repository(
|
|
64
|
+
request: RepositoryCreateRequest,
|
|
65
|
+
service: CommitIndexingAppServiceDep,
|
|
66
|
+
) -> RepositoryResponse:
|
|
67
|
+
"""Clone a new repository and perform initial mapping."""
|
|
68
|
+
try:
|
|
69
|
+
remote_uri = request.data.attributes.remote_uri
|
|
70
|
+
|
|
71
|
+
repo = await service.create_git_repository(remote_uri)
|
|
72
|
+
|
|
73
|
+
return RepositoryResponse(data=RepositoryData.from_git_repo(repo))
|
|
74
|
+
except ValueError as e:
|
|
75
|
+
if "already exists" in str(e):
|
|
76
|
+
raise HTTPException(status_code=409, detail=str(e)) from e
|
|
77
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
78
|
+
except Exception as e:
|
|
79
|
+
msg = f"Failed to clone repository: {e}"
|
|
80
|
+
raise HTTPException(status_code=500, detail=msg) from e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.get(
|
|
84
|
+
"/{repo_id}",
|
|
85
|
+
summary="Get repository",
|
|
86
|
+
responses={404: {"description": "Repository not found"}},
|
|
87
|
+
)
|
|
88
|
+
async def get_repository(
|
|
89
|
+
repo_id: str,
|
|
90
|
+
git_repository: GitRepositoryDep,
|
|
91
|
+
git_commit_repository: GitCommitRepositoryDep,
|
|
92
|
+
git_branch_repository: GitBranchRepositoryDep,
|
|
93
|
+
) -> RepositoryDetailsResponse:
|
|
94
|
+
"""Get repository details including branches and recent commits."""
|
|
95
|
+
repo = await git_repository.get_by_id(int(repo_id))
|
|
96
|
+
if not repo:
|
|
97
|
+
raise HTTPException(status_code=404, detail="Repository not found")
|
|
98
|
+
|
|
99
|
+
# Get all commits for this repository from the commit repository
|
|
100
|
+
repo_commits = await git_commit_repository.get_by_repo_id(int(repo_id))
|
|
101
|
+
commits_by_sha = {commit.commit_sha: commit for commit in repo_commits}
|
|
102
|
+
|
|
103
|
+
# Get recent commits from the tracking branch's head commit
|
|
104
|
+
recent_commits = []
|
|
105
|
+
if repo.tracking_branch and repo.tracking_branch.head_commit:
|
|
106
|
+
# For simplicity, just show the head commit and traverse back if needed
|
|
107
|
+
current_commit = repo.tracking_branch.head_commit
|
|
108
|
+
recent_commits = [current_commit]
|
|
109
|
+
|
|
110
|
+
# Traverse parent commits for more recent commits (up to 10)
|
|
111
|
+
current_sha = current_commit.parent_commit_sha
|
|
112
|
+
while current_sha and len(recent_commits) < 10:
|
|
113
|
+
parent_commit = commits_by_sha.get(current_sha)
|
|
114
|
+
if parent_commit:
|
|
115
|
+
recent_commits.append(parent_commit)
|
|
116
|
+
current_sha = parent_commit.parent_commit_sha
|
|
117
|
+
else:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# Get commit count for the repository using the commit repository
|
|
121
|
+
commit_count = await git_commit_repository.count_by_repo_id(int(repo_id))
|
|
122
|
+
|
|
123
|
+
# Get branches for the repository using the branch repository
|
|
124
|
+
repo_branches = await git_branch_repository.get_by_repo_id(int(repo_id))
|
|
125
|
+
|
|
126
|
+
# Get commit counts for all branches using the commit repository
|
|
127
|
+
branch_data = []
|
|
128
|
+
for branch in repo_branches:
|
|
129
|
+
# For simplicity, use the total commit count for all branches
|
|
130
|
+
# In a more advanced implementation, we would traverse each branch's history
|
|
131
|
+
branch_commit_count = commit_count
|
|
132
|
+
|
|
133
|
+
branch_data.append(
|
|
134
|
+
RepositoryBranchData(
|
|
135
|
+
name=branch.name,
|
|
136
|
+
is_default=branch.name == repo.tracking_branch.name
|
|
137
|
+
if repo.tracking_branch
|
|
138
|
+
else False,
|
|
139
|
+
commit_count=branch_commit_count,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return RepositoryDetailsResponse(
|
|
144
|
+
data=RepositoryData.from_git_repo(repo),
|
|
145
|
+
branches=branch_data,
|
|
146
|
+
recent_commits=[
|
|
147
|
+
RepositoryCommitData(
|
|
148
|
+
sha=commit.commit_sha,
|
|
149
|
+
message=commit.message,
|
|
150
|
+
author=commit.author,
|
|
151
|
+
timestamp=commit.date,
|
|
152
|
+
)
|
|
153
|
+
for commit in recent_commits
|
|
154
|
+
],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.get(
|
|
159
|
+
"/{repo_id}/status",
|
|
160
|
+
responses={404: {"description": "Index not found"}},
|
|
161
|
+
)
|
|
162
|
+
async def get_index_status(
|
|
163
|
+
repo_id: int,
|
|
164
|
+
status_service: TaskStatusQueryServiceDep,
|
|
165
|
+
) -> TaskStatusListResponse:
|
|
166
|
+
"""Get the status of tasks for an index."""
|
|
167
|
+
# Get all task statuses for this index
|
|
168
|
+
progress_trackers = await status_service.get_index_status(repo_id)
|
|
169
|
+
|
|
170
|
+
# Convert progress trackers to API response format
|
|
171
|
+
task_statuses = []
|
|
172
|
+
for _i, status in enumerate(progress_trackers):
|
|
173
|
+
task_statuses.append(
|
|
174
|
+
TaskStatusData(
|
|
175
|
+
id=status.id,
|
|
176
|
+
attributes=TaskStatusAttributes(
|
|
177
|
+
step=status.operation,
|
|
178
|
+
state=status.state,
|
|
179
|
+
progress=status.completion_percent,
|
|
180
|
+
total=status.total,
|
|
181
|
+
current=status.current,
|
|
182
|
+
created_at=status.created_at,
|
|
183
|
+
updated_at=status.updated_at,
|
|
184
|
+
error=status.error or "",
|
|
185
|
+
message=status.message,
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return TaskStatusListResponse(data=task_statuses)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.get(
|
|
194
|
+
"/{repo_id}/tags",
|
|
195
|
+
summary="List repository tags",
|
|
196
|
+
responses={404: {"description": "Repository not found"}},
|
|
197
|
+
)
|
|
198
|
+
async def list_repository_tags(
|
|
199
|
+
repo_id: str,
|
|
200
|
+
git_repository: GitRepositoryDep,
|
|
201
|
+
git_tag_repository: GitTagRepositoryDep,
|
|
202
|
+
) -> TagListResponse:
|
|
203
|
+
"""List all tags for a repository."""
|
|
204
|
+
repo = await git_repository.get_by_id(int(repo_id))
|
|
205
|
+
if not repo:
|
|
206
|
+
raise HTTPException(status_code=404, detail="Repository not found")
|
|
207
|
+
|
|
208
|
+
# Tags are now stored in a dedicated repository
|
|
209
|
+
tags = await git_tag_repository.get_by_repo_id(int(repo_id))
|
|
210
|
+
|
|
211
|
+
return TagListResponse(
|
|
212
|
+
data=[
|
|
213
|
+
TagData(
|
|
214
|
+
type="tag",
|
|
215
|
+
id=tag.id,
|
|
216
|
+
attributes=TagAttributes(
|
|
217
|
+
name=tag.name,
|
|
218
|
+
target_commit_sha=tag.target_commit.commit_sha,
|
|
219
|
+
is_version_tag=tag.is_version_tag,
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
for tag in tags
|
|
223
|
+
]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@router.get(
|
|
228
|
+
"/{repo_id}/tags/{tag_id}",
|
|
229
|
+
summary="Get repository tag",
|
|
230
|
+
responses={404: {"description": "Repository or tag not found"}},
|
|
231
|
+
)
|
|
232
|
+
async def get_repository_tag(
|
|
233
|
+
repo_id: str,
|
|
234
|
+
tag_id: str,
|
|
235
|
+
git_repository: GitRepositoryDep,
|
|
236
|
+
git_tag_repository: GitTagRepositoryDep,
|
|
237
|
+
) -> TagResponse:
|
|
238
|
+
"""Get a specific tag for a repository."""
|
|
239
|
+
repo = await git_repository.get_by_id(int(repo_id))
|
|
240
|
+
if not repo:
|
|
241
|
+
raise HTTPException(status_code=404, detail="Repository not found")
|
|
242
|
+
|
|
243
|
+
# Get all tags and find the specific one by ID
|
|
244
|
+
tags = await git_tag_repository.get_by_repo_id(int(repo_id))
|
|
245
|
+
tag = next((t for t in tags if t.id == tag_id), None)
|
|
246
|
+
if not tag:
|
|
247
|
+
raise HTTPException(status_code=404, detail="Tag not found")
|
|
248
|
+
|
|
249
|
+
return TagResponse(
|
|
250
|
+
data=TagData(
|
|
251
|
+
type="tag",
|
|
252
|
+
id=tag.id,
|
|
253
|
+
attributes=TagAttributes(
|
|
254
|
+
name=tag.name,
|
|
255
|
+
target_commit_sha=tag.target_commit.commit_sha,
|
|
256
|
+
is_version_tag=tag.is_version_tag,
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@router.delete(
|
|
263
|
+
"/{repo_id}",
|
|
264
|
+
status_code=204,
|
|
265
|
+
summary="Delete repository",
|
|
266
|
+
responses={404: {"description": "Repository not found"}},
|
|
267
|
+
)
|
|
268
|
+
async def delete_repository(
|
|
269
|
+
repo_id: str,
|
|
270
|
+
service: CommitIndexingAppServiceDep,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Delete a repository and all its associated data."""
|
|
273
|
+
try:
|
|
274
|
+
repo_id_int = int(repo_id)
|
|
275
|
+
deleted = await service.delete_git_repository(repo_id_int)
|
|
276
|
+
if not deleted:
|
|
277
|
+
_raise_not_found_error("Repository not found")
|
|
278
|
+
except ValueError:
|
|
279
|
+
raise HTTPException(status_code=400, detail="Invalid repository ID") from None
|
|
280
|
+
except Exception as e:
|
|
281
|
+
msg = f"Failed to delete repository: {e}"
|
|
282
|
+
raise HTTPException(status_code=500, detail=msg) from e
|