kodit 0.5.4__py3-none-any.whl → 0.5.5__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/application/factories/server_factory.py +54 -32
- kodit/application/services/code_search_application_service.py +89 -12
- kodit/application/services/commit_indexing_application_service.py +314 -195
- kodit/application/services/enrichment_query_service.py +274 -43
- kodit/application/services/indexing_worker_service.py +1 -1
- kodit/application/services/queue_service.py +15 -10
- kodit/application/services/sync_scheduler.py +2 -1
- kodit/domain/enrichments/architecture/architecture.py +1 -1
- kodit/domain/enrichments/architecture/physical/physical.py +1 -1
- kodit/domain/enrichments/development/development.py +1 -1
- kodit/domain/enrichments/development/snippet/snippet.py +12 -5
- kodit/domain/enrichments/enrichment.py +31 -4
- kodit/domain/enrichments/usage/api_docs.py +1 -1
- kodit/domain/enrichments/usage/usage.py +1 -1
- kodit/domain/entities/git.py +30 -25
- kodit/domain/factories/git_repo_factory.py +20 -5
- kodit/domain/protocols.py +56 -125
- kodit/domain/services/embedding_service.py +14 -16
- kodit/domain/services/git_repository_service.py +60 -38
- kodit/domain/services/git_service.py +18 -11
- kodit/domain/tracking/resolution_service.py +6 -16
- kodit/domain/value_objects.py +2 -9
- kodit/infrastructure/api/v1/dependencies.py +12 -3
- kodit/infrastructure/api/v1/query_params.py +27 -0
- kodit/infrastructure/api/v1/routers/commits.py +91 -85
- kodit/infrastructure/api/v1/routers/repositories.py +53 -37
- kodit/infrastructure/api/v1/routers/search.py +1 -1
- kodit/infrastructure/api/v1/schemas/enrichment.py +14 -0
- kodit/infrastructure/api/v1/schemas/repository.py +1 -1
- kodit/infrastructure/slicing/api_doc_extractor.py +0 -2
- kodit/infrastructure/sqlalchemy/embedding_repository.py +44 -34
- kodit/infrastructure/sqlalchemy/enrichment_association_repository.py +73 -0
- kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +116 -97
- kodit/infrastructure/sqlalchemy/entities.py +12 -116
- kodit/infrastructure/sqlalchemy/git_branch_repository.py +52 -244
- kodit/infrastructure/sqlalchemy/git_commit_repository.py +35 -324
- kodit/infrastructure/sqlalchemy/git_file_repository.py +70 -0
- kodit/infrastructure/sqlalchemy/git_repository.py +60 -230
- kodit/infrastructure/sqlalchemy/git_tag_repository.py +53 -240
- kodit/infrastructure/sqlalchemy/query.py +331 -0
- kodit/infrastructure/sqlalchemy/repository.py +203 -0
- kodit/infrastructure/sqlalchemy/task_repository.py +79 -58
- kodit/infrastructure/sqlalchemy/task_status_repository.py +45 -52
- kodit/migrations/versions/4b1a3b2c8fa5_refactor_git_tracking.py +190 -0
- {kodit-0.5.4.dist-info → kodit-0.5.5.dist-info}/METADATA +1 -1
- {kodit-0.5.4.dist-info → kodit-0.5.5.dist-info}/RECORD +50 -48
- kodit/infrastructure/mappers/enrichment_mapper.py +0 -83
- kodit/infrastructure/mappers/git_mapper.py +0 -193
- kodit/infrastructure/mappers/snippet_mapper.py +0 -104
- kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +0 -479
- {kodit-0.5.4.dist-info → kodit-0.5.5.dist-info}/WHEEL +0 -0
- {kodit-0.5.4.dist-info → kodit-0.5.5.dist-info}/entry_points.txt +0 -0
- {kodit-0.5.4.dist-info → kodit-0.5.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"""SQLAlchemy implementation of GitRepoRepository."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
+
from typing import Any, override
|
|
4
5
|
|
|
5
6
|
from pydantic import AnyUrl
|
|
6
|
-
from sqlalchemy import delete, select
|
|
7
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
8
|
|
|
9
|
-
from kodit.domain.entities.git import GitRepo
|
|
9
|
+
from kodit.domain.entities.git import GitRepo, TrackingConfig, TrackingType
|
|
10
10
|
from kodit.domain.protocols import GitRepoRepository
|
|
11
|
-
from kodit.infrastructure.mappers.git_mapper import GitMapper
|
|
12
11
|
from kodit.infrastructure.sqlalchemy import entities as db_entities
|
|
12
|
+
from kodit.infrastructure.sqlalchemy.repository import SqlAlchemyRepository
|
|
13
13
|
from kodit.infrastructure.sqlalchemy.unit_of_work import SqlAlchemyUnitOfWork
|
|
14
14
|
|
|
15
15
|
|
|
@@ -20,243 +20,73 @@ def create_git_repo_repository(
|
|
|
20
20
|
return SqlAlchemyGitRepoRepository(session_factory=session_factory)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class SqlAlchemyGitRepoRepository(
|
|
24
|
-
|
|
23
|
+
class SqlAlchemyGitRepoRepository(
|
|
24
|
+
SqlAlchemyRepository[GitRepo, db_entities.GitRepo], GitRepoRepository
|
|
25
|
+
):
|
|
26
|
+
"""SQLAlchemy implementation of GitRepoRepository."""
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- GitBranch entities
|
|
29
|
-
- GitTag entities
|
|
30
|
-
|
|
31
|
-
Note: Commits are now managed by the separate GitCommitRepository.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
def __init__(self, session_factory: Callable[[], AsyncSession]) -> None:
|
|
35
|
-
"""Initialize the repository."""
|
|
36
|
-
self.session_factory = session_factory
|
|
28
|
+
def _get_id(self, entity: GitRepo) -> Any:
|
|
29
|
+
return entity.id
|
|
37
30
|
|
|
38
31
|
@property
|
|
39
|
-
def
|
|
40
|
-
|
|
32
|
+
def db_entity_type(self) -> type[db_entities.GitRepo]:
|
|
33
|
+
"""The SQLAlchemy model type."""
|
|
34
|
+
return db_entities.GitRepo
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
@override
|
|
37
|
+
async def save(self, entity: GitRepo) -> GitRepo:
|
|
38
|
+
"""Save entity (create new or update existing)."""
|
|
44
39
|
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
entity_id = self._get_id(entity)
|
|
41
|
+
# Skip session.get if entity_id is None (new entity not yet persisted)
|
|
42
|
+
existing_db_entity = (
|
|
43
|
+
await session.get(self.db_entity_type, entity_id)
|
|
44
|
+
if entity_id is not None
|
|
45
|
+
else None
|
|
50
46
|
)
|
|
51
|
-
existing_repo = await session.scalar(existing_repo_stmt)
|
|
52
47
|
|
|
53
|
-
if
|
|
54
|
-
# Update existing
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
existing_repo.num_commits = repo.num_commits
|
|
59
|
-
existing_repo.num_branches = repo.num_branches
|
|
60
|
-
existing_repo.num_tags = repo.num_tags
|
|
61
|
-
db_repo = existing_repo
|
|
62
|
-
repo.id = existing_repo.id # Set the domain ID
|
|
48
|
+
if existing_db_entity:
|
|
49
|
+
# Update existing entity
|
|
50
|
+
new_db_entity = self.to_db(entity)
|
|
51
|
+
self._update_db_entity(existing_db_entity, new_db_entity)
|
|
52
|
+
db_entity = existing_db_entity
|
|
63
53
|
else:
|
|
64
|
-
# Create new
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
remote_uri=str(repo.remote_uri),
|
|
68
|
-
cloned_path=repo.cloned_path,
|
|
69
|
-
last_scanned_at=repo.last_scanned_at,
|
|
70
|
-
num_commits=repo.num_commits,
|
|
71
|
-
num_branches=repo.num_branches,
|
|
72
|
-
num_tags=repo.num_tags,
|
|
73
|
-
)
|
|
74
|
-
session.add(db_repo)
|
|
75
|
-
await session.flush() # Get the new ID
|
|
76
|
-
repo.id = db_repo.id # Set the domain ID
|
|
77
|
-
|
|
78
|
-
# 2. Save tracking branch
|
|
79
|
-
await self._save_tracking_branch(session, repo)
|
|
54
|
+
# Create new entity
|
|
55
|
+
db_entity = self.to_db(entity)
|
|
56
|
+
session.add(db_entity)
|
|
80
57
|
|
|
81
58
|
await session.flush()
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
async def get_by_id(self, repo_id: int) -> GitRepo:
|
|
103
|
-
"""Get repository by ID with all associated data."""
|
|
104
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
105
|
-
db_repo = await session.get(db_entities.GitRepo, repo_id)
|
|
106
|
-
if not db_repo:
|
|
107
|
-
raise ValueError(f"Repository with ID {repo_id} not found")
|
|
108
|
-
|
|
109
|
-
return await self._load_complete_repo(session, db_repo)
|
|
110
|
-
|
|
111
|
-
async def get_by_uri(self, sanitized_uri: AnyUrl) -> GitRepo:
|
|
112
|
-
"""Get repository by sanitized URI with all associated data."""
|
|
113
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
114
|
-
stmt = select(db_entities.GitRepo).where(
|
|
115
|
-
db_entities.GitRepo.sanitized_remote_uri == str(sanitized_uri)
|
|
116
|
-
)
|
|
117
|
-
db_repo = await session.scalar(stmt)
|
|
118
|
-
if not db_repo:
|
|
119
|
-
raise ValueError(f"Repository with URI {sanitized_uri} not found")
|
|
120
|
-
|
|
121
|
-
return await self._load_complete_repo(session, db_repo)
|
|
122
|
-
|
|
123
|
-
async def get_by_commit(self, commit_sha: str) -> GitRepo:
|
|
124
|
-
"""Get repository by commit SHA with all associated data."""
|
|
125
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
126
|
-
# Find the commit first
|
|
127
|
-
stmt = select(db_entities.GitCommit).where(
|
|
128
|
-
db_entities.GitCommit.commit_sha == commit_sha
|
|
129
|
-
)
|
|
130
|
-
db_commit = await session.scalar(stmt)
|
|
131
|
-
if not db_commit:
|
|
132
|
-
raise ValueError(f"Commit with SHA {commit_sha} not found")
|
|
133
|
-
|
|
134
|
-
# Get the repo
|
|
135
|
-
db_repo = await session.get(db_entities.GitRepo, db_commit.repo_id)
|
|
136
|
-
if not db_repo:
|
|
137
|
-
raise ValueError(f"Repository with commit SHA {commit_sha} not found")
|
|
138
|
-
|
|
139
|
-
return await self._load_complete_repo(session, db_repo)
|
|
140
|
-
|
|
141
|
-
async def get_all(self) -> list[GitRepo]:
|
|
142
|
-
"""Get all repositories."""
|
|
143
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
144
|
-
stmt = select(db_entities.GitRepo)
|
|
145
|
-
db_repos = (await session.scalars(stmt)).all()
|
|
146
|
-
|
|
147
|
-
repos = []
|
|
148
|
-
for db_repo in db_repos:
|
|
149
|
-
repo = await self._load_complete_repo(session, db_repo)
|
|
150
|
-
repos.append(repo)
|
|
151
|
-
|
|
152
|
-
return repos
|
|
153
|
-
|
|
154
|
-
async def delete(self, sanitized_uri: AnyUrl) -> bool:
|
|
155
|
-
"""Delete only the repository entity itself.
|
|
156
|
-
|
|
157
|
-
According to DDD principles, this repository should only delete
|
|
158
|
-
the GitRepo entity it directly controls. Related entities (commits,
|
|
159
|
-
branches, tags, snippets) should be deleted by their respective
|
|
160
|
-
repositories before calling this method.
|
|
161
|
-
"""
|
|
162
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
163
|
-
# Find the repo
|
|
164
|
-
stmt = select(db_entities.GitRepo).where(
|
|
165
|
-
db_entities.GitRepo.sanitized_remote_uri == str(sanitized_uri)
|
|
166
|
-
)
|
|
167
|
-
db_repo = await session.scalar(stmt)
|
|
168
|
-
if not db_repo:
|
|
169
|
-
return False
|
|
170
|
-
|
|
171
|
-
# Delete tracking branches first (they reference the repo)
|
|
172
|
-
del_tracking_branches_stmt = delete(db_entities.GitTrackingBranch).where(
|
|
173
|
-
db_entities.GitTrackingBranch.repo_id == db_repo.id
|
|
174
|
-
)
|
|
175
|
-
await session.execute(del_tracking_branches_stmt)
|
|
176
|
-
|
|
177
|
-
# Delete only the repo entity itself
|
|
178
|
-
# Foreign key constraints will prevent deletion if related entities exist
|
|
179
|
-
del_stmt = delete(db_entities.GitRepo).where(
|
|
180
|
-
db_entities.GitRepo.id == db_repo.id
|
|
181
|
-
)
|
|
182
|
-
await session.execute(del_stmt)
|
|
183
|
-
return True
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
async def _load_complete_repo(
|
|
187
|
-
self, session: AsyncSession, db_repo: db_entities.GitRepo
|
|
188
|
-
) -> GitRepo:
|
|
189
|
-
"""Load a complete repo with all its associations."""
|
|
190
|
-
all_tags = list(
|
|
191
|
-
(
|
|
192
|
-
await session.scalars(
|
|
193
|
-
select(db_entities.GitTag).where(
|
|
194
|
-
db_entities.GitTag.repo_id == db_repo.id
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
).all()
|
|
59
|
+
return self.to_domain(db_entity)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def to_domain(db_entity: db_entities.GitRepo) -> GitRepo:
|
|
63
|
+
"""Map database entity to domain entity."""
|
|
64
|
+
return GitRepo(
|
|
65
|
+
id=db_entity.id,
|
|
66
|
+
sanitized_remote_uri=AnyUrl(db_entity.sanitized_remote_uri),
|
|
67
|
+
remote_uri=AnyUrl(db_entity.remote_uri),
|
|
68
|
+
cloned_path=db_entity.cloned_path,
|
|
69
|
+
last_scanned_at=db_entity.last_scanned_at,
|
|
70
|
+
num_commits=db_entity.num_commits,
|
|
71
|
+
num_branches=db_entity.num_branches,
|
|
72
|
+
num_tags=db_entity.num_tags,
|
|
73
|
+
tracking_config=TrackingConfig(
|
|
74
|
+
type=TrackingType(db_entity.tracking_type),
|
|
75
|
+
name=db_entity.tracking_name,
|
|
76
|
+
),
|
|
198
77
|
)
|
|
199
|
-
tracking_branch = await session.scalar(
|
|
200
|
-
select(db_entities.GitTrackingBranch).where(
|
|
201
|
-
db_entities.GitTrackingBranch.repo_id == db_repo.id
|
|
202
|
-
)
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
# Get tracking branch from branches table if needed
|
|
206
|
-
db_tracking_branch_entity = None
|
|
207
|
-
if tracking_branch:
|
|
208
|
-
db_tracking_branch_entity = await session.scalar(
|
|
209
|
-
select(db_entities.GitBranch).where(
|
|
210
|
-
db_entities.GitBranch.repo_id == db_repo.id,
|
|
211
|
-
db_entities.GitBranch.name == tracking_branch.name,
|
|
212
|
-
)
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Get only commits needed for tags and tracking branch
|
|
216
|
-
referenced_commit_shas = set()
|
|
217
|
-
for tag in all_tags:
|
|
218
|
-
referenced_commit_shas.add(tag.target_commit_sha)
|
|
219
|
-
if db_tracking_branch_entity:
|
|
220
|
-
referenced_commit_shas.add(db_tracking_branch_entity.head_commit_sha)
|
|
221
|
-
|
|
222
|
-
# Load only the referenced commits in chunks to avoid parameter limits
|
|
223
|
-
referenced_commits = []
|
|
224
|
-
referenced_files = []
|
|
225
|
-
if referenced_commit_shas:
|
|
226
|
-
commit_shas_list = list(referenced_commit_shas)
|
|
227
|
-
chunk_size = 1000
|
|
228
|
-
|
|
229
|
-
for i in range(0, len(commit_shas_list), chunk_size):
|
|
230
|
-
chunk = commit_shas_list[i : i + chunk_size]
|
|
231
|
-
chunk_commits = list(
|
|
232
|
-
(
|
|
233
|
-
await session.scalars(
|
|
234
|
-
select(db_entities.GitCommit).where(
|
|
235
|
-
db_entities.GitCommit.commit_sha.in_(chunk)
|
|
236
|
-
)
|
|
237
|
-
)
|
|
238
|
-
).all()
|
|
239
|
-
)
|
|
240
|
-
referenced_commits.extend(chunk_commits)
|
|
241
|
-
|
|
242
|
-
for i in range(0, len(commit_shas_list), chunk_size):
|
|
243
|
-
chunk = commit_shas_list[i : i + chunk_size]
|
|
244
|
-
chunk_files = list(
|
|
245
|
-
(
|
|
246
|
-
await session.scalars(
|
|
247
|
-
select(db_entities.GitCommitFile).where(
|
|
248
|
-
db_entities.GitCommitFile.commit_sha.in_(chunk)
|
|
249
|
-
)
|
|
250
|
-
)
|
|
251
|
-
).all()
|
|
252
|
-
)
|
|
253
|
-
referenced_files.extend(chunk_files)
|
|
254
78
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
79
|
+
@staticmethod
|
|
80
|
+
def to_db(domain_entity: GitRepo) -> db_entities.GitRepo:
|
|
81
|
+
"""Map domain entity to database entity."""
|
|
82
|
+
return db_entities.GitRepo(
|
|
83
|
+
sanitized_remote_uri=str(domain_entity.sanitized_remote_uri),
|
|
84
|
+
remote_uri=str(domain_entity.remote_uri),
|
|
85
|
+
cloned_path=domain_entity.cloned_path,
|
|
86
|
+
last_scanned_at=domain_entity.last_scanned_at,
|
|
87
|
+
num_commits=domain_entity.num_commits,
|
|
88
|
+
num_branches=domain_entity.num_branches,
|
|
89
|
+
num_tags=domain_entity.num_tags,
|
|
90
|
+
tracking_type=domain_entity.tracking_config.type,
|
|
91
|
+
tracking_name=domain_entity.tracking_config.name,
|
|
262
92
|
)
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
|
|
5
|
-
from sqlalchemy import delete, func, insert, select
|
|
6
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
6
|
|
|
8
|
-
from kodit.domain.entities.git import
|
|
7
|
+
from kodit.domain.entities.git import GitTag
|
|
9
8
|
from kodit.domain.protocols import GitTagRepository
|
|
10
9
|
from kodit.infrastructure.sqlalchemy import entities as db_entities
|
|
11
|
-
from kodit.infrastructure.sqlalchemy.
|
|
10
|
+
from kodit.infrastructure.sqlalchemy.query import FilterOperator, QueryBuilder
|
|
11
|
+
from kodit.infrastructure.sqlalchemy.repository import SqlAlchemyRepository
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def create_git_tag_repository(
|
|
@@ -18,251 +18,64 @@ def create_git_tag_repository(
|
|
|
18
18
|
return SqlAlchemyGitTagRepository(session_factory=session_factory)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class SqlAlchemyGitTagRepository(
|
|
21
|
+
class SqlAlchemyGitTagRepository(
|
|
22
|
+
SqlAlchemyRepository[GitTag, db_entities.GitTag], GitTagRepository
|
|
23
|
+
):
|
|
22
24
|
"""SQLAlchemy implementation of GitTagRepository."""
|
|
23
25
|
|
|
24
|
-
def
|
|
25
|
-
"""
|
|
26
|
-
|
|
26
|
+
def _get_id(self, entity: GitTag) -> tuple[int, str]:
|
|
27
|
+
"""Get the ID of a tag."""
|
|
28
|
+
if entity.repo_id is None:
|
|
29
|
+
raise ValueError("Repository ID is required")
|
|
30
|
+
return (entity.repo_id, entity.name)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def db_entity_type(self) -> type[db_entities.GitTag]:
|
|
34
|
+
"""The SQLAlchemy model type."""
|
|
35
|
+
return db_entities.GitTag
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def to_domain(db_entity: db_entities.GitTag) -> GitTag:
|
|
39
|
+
"""Map a SQLAlchemy GitTag to a domain GitTag."""
|
|
40
|
+
return GitTag(
|
|
41
|
+
created_at=db_entity.created_at,
|
|
42
|
+
updated_at=db_entity.updated_at,
|
|
43
|
+
repo_id=db_entity.repo_id,
|
|
44
|
+
name=db_entity.name,
|
|
45
|
+
target_commit_sha=db_entity.target_commit_sha,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def to_db(domain_entity: GitTag) -> db_entities.GitTag:
|
|
50
|
+
"""Map a domain GitTag to a SQLAlchemy GitTag."""
|
|
51
|
+
if domain_entity.repo_id is None:
|
|
52
|
+
raise ValueError("Repository ID is required")
|
|
53
|
+
return db_entities.GitTag(
|
|
54
|
+
repo_id=domain_entity.repo_id,
|
|
55
|
+
name=domain_entity.name,
|
|
56
|
+
target_commit_sha=domain_entity.target_commit_sha,
|
|
57
|
+
)
|
|
27
58
|
|
|
28
59
|
async def get_by_name(self, tag_name: str, repo_id: int) -> GitTag:
|
|
29
60
|
"""Get a tag by name and repository ID."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Get the target commit
|
|
41
|
-
commit_stmt = select(db_entities.GitCommit).where(
|
|
42
|
-
db_entities.GitCommit.commit_sha == db_tag.target_commit_sha
|
|
43
|
-
)
|
|
44
|
-
db_commit = await session.scalar(commit_stmt)
|
|
45
|
-
if not db_commit:
|
|
46
|
-
raise ValueError(f"Target commit {db_tag.target_commit_sha} not found")
|
|
47
|
-
|
|
48
|
-
# Get files for the target commit
|
|
49
|
-
files_stmt = select(db_entities.GitCommitFile).where(
|
|
50
|
-
db_entities.GitCommitFile.commit_sha == db_tag.target_commit_sha
|
|
51
|
-
)
|
|
52
|
-
db_files = (await session.scalars(files_stmt)).all()
|
|
53
|
-
|
|
54
|
-
domain_files = []
|
|
55
|
-
for db_file in db_files:
|
|
56
|
-
domain_file = GitFile(
|
|
57
|
-
blob_sha=db_file.blob_sha,
|
|
58
|
-
path=db_file.path,
|
|
59
|
-
mime_type=db_file.mime_type,
|
|
60
|
-
size=db_file.size,
|
|
61
|
-
extension=db_file.extension,
|
|
62
|
-
created_at=db_file.created_at,
|
|
63
|
-
)
|
|
64
|
-
domain_files.append(domain_file)
|
|
65
|
-
|
|
66
|
-
target_commit = GitCommit(
|
|
67
|
-
commit_sha=db_commit.commit_sha,
|
|
68
|
-
date=db_commit.date,
|
|
69
|
-
message=db_commit.message,
|
|
70
|
-
parent_commit_sha=db_commit.parent_commit_sha,
|
|
71
|
-
files=domain_files,
|
|
72
|
-
author=db_commit.author,
|
|
73
|
-
created_at=db_commit.created_at,
|
|
74
|
-
updated_at=db_commit.updated_at,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return GitTag(
|
|
78
|
-
repo_id=db_tag.repo_id,
|
|
79
|
-
name=db_tag.name,
|
|
80
|
-
target_commit=target_commit,
|
|
81
|
-
created_at=db_tag.created_at,
|
|
82
|
-
updated_at=db_tag.updated_at,
|
|
83
|
-
)
|
|
61
|
+
query = (
|
|
62
|
+
QueryBuilder()
|
|
63
|
+
.filter("name", FilterOperator.EQ, tag_name)
|
|
64
|
+
.filter("repo_id", FilterOperator.EQ, repo_id)
|
|
65
|
+
)
|
|
66
|
+
tags = await self.find(query)
|
|
67
|
+
if not tags:
|
|
68
|
+
raise ValueError(f"Tag {tag_name} not found in repo {repo_id}")
|
|
69
|
+
return tags[0]
|
|
84
70
|
|
|
85
71
|
async def get_by_repo_id(self, repo_id: int) -> list[GitTag]:
|
|
86
72
|
"""Get all tags for a repository."""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
db_entities.GitTag.repo_id == repo_id
|
|
91
|
-
)
|
|
92
|
-
db_tags = (await session.scalars(tags_stmt)).all()
|
|
93
|
-
|
|
94
|
-
if not db_tags:
|
|
95
|
-
return []
|
|
96
|
-
|
|
97
|
-
commit_shas = [tag.target_commit_sha for tag in db_tags]
|
|
98
|
-
|
|
99
|
-
# Get all target commits for these tags in chunks
|
|
100
|
-
# to avoid parameter limits
|
|
101
|
-
db_commits: list[db_entities.GitCommit] = []
|
|
102
|
-
chunk_size = 1000
|
|
103
|
-
for i in range(0, len(commit_shas), chunk_size):
|
|
104
|
-
chunk = commit_shas[i : i + chunk_size]
|
|
105
|
-
commits_stmt = select(db_entities.GitCommit).where(
|
|
106
|
-
db_entities.GitCommit.commit_sha.in_(chunk)
|
|
107
|
-
)
|
|
108
|
-
chunk_commits = (await session.scalars(commits_stmt)).all()
|
|
109
|
-
db_commits.extend(chunk_commits)
|
|
110
|
-
|
|
111
|
-
# Get all files for these commits in chunks
|
|
112
|
-
# to avoid parameter limits
|
|
113
|
-
db_files: list[db_entities.GitCommitFile] = []
|
|
114
|
-
for i in range(0, len(commit_shas), chunk_size):
|
|
115
|
-
chunk = commit_shas[i : i + chunk_size]
|
|
116
|
-
files_stmt = select(db_entities.GitCommitFile).where(
|
|
117
|
-
db_entities.GitCommitFile.commit_sha.in_(chunk)
|
|
118
|
-
)
|
|
119
|
-
chunk_files = (await session.scalars(files_stmt)).all()
|
|
120
|
-
db_files.extend(chunk_files)
|
|
121
|
-
|
|
122
|
-
# Group files by commit SHA
|
|
123
|
-
files_by_commit: dict[str, list[GitFile]] = {}
|
|
124
|
-
for db_file in db_files:
|
|
125
|
-
if db_file.commit_sha not in files_by_commit:
|
|
126
|
-
files_by_commit[db_file.commit_sha] = []
|
|
127
|
-
|
|
128
|
-
domain_file = GitFile(
|
|
129
|
-
blob_sha=db_file.blob_sha,
|
|
130
|
-
path=db_file.path,
|
|
131
|
-
mime_type=db_file.mime_type,
|
|
132
|
-
size=db_file.size,
|
|
133
|
-
extension=db_file.extension,
|
|
134
|
-
created_at=db_file.created_at,
|
|
135
|
-
)
|
|
136
|
-
files_by_commit[db_file.commit_sha].append(domain_file)
|
|
137
|
-
|
|
138
|
-
# Create commit lookup
|
|
139
|
-
commits_by_sha = {commit.commit_sha: commit for commit in db_commits}
|
|
140
|
-
|
|
141
|
-
# Create domain tags
|
|
142
|
-
domain_tags = []
|
|
143
|
-
for db_tag in db_tags:
|
|
144
|
-
db_commit = commits_by_sha.get(db_tag.target_commit_sha)
|
|
145
|
-
if not db_commit:
|
|
146
|
-
continue
|
|
147
|
-
|
|
148
|
-
commit_files = files_by_commit.get(db_tag.target_commit_sha, [])
|
|
149
|
-
target_commit = GitCommit(
|
|
150
|
-
commit_sha=db_commit.commit_sha,
|
|
151
|
-
date=db_commit.date,
|
|
152
|
-
message=db_commit.message,
|
|
153
|
-
parent_commit_sha=db_commit.parent_commit_sha,
|
|
154
|
-
files=commit_files,
|
|
155
|
-
author=db_commit.author,
|
|
156
|
-
created_at=db_commit.created_at,
|
|
157
|
-
updated_at=db_commit.updated_at,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
domain_tag = GitTag(
|
|
161
|
-
repo_id=db_tag.repo_id,
|
|
162
|
-
name=db_tag.name,
|
|
163
|
-
target_commit=target_commit,
|
|
164
|
-
created_at=db_tag.created_at,
|
|
165
|
-
updated_at=db_tag.updated_at,
|
|
166
|
-
)
|
|
167
|
-
domain_tags.append(domain_tag)
|
|
168
|
-
|
|
169
|
-
return domain_tags
|
|
170
|
-
|
|
171
|
-
async def save(self, tag: GitTag, repo_id: int) -> GitTag:
|
|
172
|
-
"""Save a tag to a repository."""
|
|
173
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
174
|
-
# Set repo_id on the tag
|
|
175
|
-
tag.repo_id = repo_id
|
|
176
|
-
|
|
177
|
-
# Check if tag already exists
|
|
178
|
-
existing_tag = await session.get(
|
|
179
|
-
db_entities.GitTag, (repo_id, tag.name)
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
if existing_tag:
|
|
183
|
-
# Update existing tag
|
|
184
|
-
existing_tag.target_commit_sha = tag.target_commit.commit_sha
|
|
185
|
-
if tag.updated_at:
|
|
186
|
-
existing_tag.updated_at = tag.updated_at
|
|
187
|
-
else:
|
|
188
|
-
# Create new tag
|
|
189
|
-
db_tag = db_entities.GitTag(
|
|
190
|
-
repo_id=repo_id,
|
|
191
|
-
name=tag.name,
|
|
192
|
-
target_commit_sha=tag.target_commit.commit_sha,
|
|
193
|
-
)
|
|
194
|
-
session.add(db_tag)
|
|
195
|
-
|
|
196
|
-
return tag
|
|
197
|
-
|
|
198
|
-
async def save_bulk(self, tags: list[GitTag], repo_id: int) -> None:
|
|
199
|
-
"""Bulk save tags to a repository."""
|
|
200
|
-
if not tags:
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
204
|
-
# Get existing tags in bulk
|
|
205
|
-
existing_tags_stmt = select(db_entities.GitTag).where(
|
|
206
|
-
db_entities.GitTag.repo_id == repo_id,
|
|
207
|
-
db_entities.GitTag.name.in_([tag.name for tag in tags]),
|
|
208
|
-
)
|
|
209
|
-
existing_tags = (await session.scalars(existing_tags_stmt)).all()
|
|
210
|
-
existing_tag_names = {tag.name for tag in existing_tags}
|
|
211
|
-
|
|
212
|
-
# Update existing tags
|
|
213
|
-
for existing_tag in existing_tags:
|
|
214
|
-
for tag in tags:
|
|
215
|
-
if (
|
|
216
|
-
tag.name == existing_tag.name
|
|
217
|
-
and existing_tag.target_commit_sha
|
|
218
|
-
!= tag.target_commit.commit_sha
|
|
219
|
-
):
|
|
220
|
-
existing_tag.target_commit_sha = tag.target_commit.commit_sha
|
|
221
|
-
break
|
|
222
|
-
|
|
223
|
-
# Prepare new tags for bulk insert
|
|
224
|
-
new_tags_data = [
|
|
225
|
-
{
|
|
226
|
-
"repo_id": repo_id,
|
|
227
|
-
"name": tag.name,
|
|
228
|
-
"target_commit_sha": tag.target_commit.commit_sha,
|
|
229
|
-
}
|
|
230
|
-
for tag in tags
|
|
231
|
-
if tag.name not in existing_tag_names
|
|
232
|
-
]
|
|
233
|
-
|
|
234
|
-
# Bulk insert new tags in chunks to avoid parameter limits
|
|
235
|
-
if new_tags_data:
|
|
236
|
-
chunk_size = 1000 # Conservative chunk size for parameter limits
|
|
237
|
-
for i in range(0, len(new_tags_data), chunk_size):
|
|
238
|
-
chunk = new_tags_data[i : i + chunk_size]
|
|
239
|
-
stmt = insert(db_entities.GitTag).values(chunk)
|
|
240
|
-
await session.execute(stmt)
|
|
241
|
-
|
|
242
|
-
async def exists(self, tag_name: str, repo_id: int) -> bool:
|
|
243
|
-
"""Check if a tag exists."""
|
|
244
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
245
|
-
stmt = select(db_entities.GitTag.name).where(
|
|
246
|
-
db_entities.GitTag.name == tag_name,
|
|
247
|
-
db_entities.GitTag.repo_id == repo_id,
|
|
248
|
-
)
|
|
249
|
-
result = await session.scalar(stmt)
|
|
250
|
-
return result is not None
|
|
73
|
+
return await self.find(
|
|
74
|
+
QueryBuilder().filter("repo_id", FilterOperator.EQ, repo_id)
|
|
75
|
+
)
|
|
251
76
|
|
|
252
77
|
async def delete_by_repo_id(self, repo_id: int) -> None:
|
|
253
78
|
"""Delete all tags for a repository."""
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
db_entities.GitTag.repo_id == repo_id
|
|
258
|
-
)
|
|
259
|
-
await session.execute(del_tags_stmt)
|
|
260
|
-
|
|
261
|
-
async def count_by_repo_id(self, repo_id: int) -> int:
|
|
262
|
-
"""Count the number of tags for a repository."""
|
|
263
|
-
async with SqlAlchemyUnitOfWork(self.session_factory) as session:
|
|
264
|
-
stmt = select(func.count()).select_from(db_entities.GitTag).where(
|
|
265
|
-
db_entities.GitTag.repo_id == repo_id
|
|
266
|
-
)
|
|
267
|
-
result = await session.scalar(stmt)
|
|
268
|
-
return result or 0
|
|
79
|
+
await self.delete_by_query(
|
|
80
|
+
QueryBuilder().filter("repo_id", FilterOperator.EQ, repo_id)
|
|
81
|
+
)
|