-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgit_history.py
More file actions
220 lines (192 loc) · 7.8 KB
/
Copy pathgit_history.py
File metadata and controls
220 lines (192 loc) · 7.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from datetime import datetime, timezone
from typing import Any
import httpx
from server.config import settings
from server.embeddings.base import EmbeddingProvider
from server.embeddings.factory import get_embedding_provider
from server.indexer.github_source import (
GitHubCommit,
fetch_commits_with_diffs,
list_commits,
)
from server.indexer.cleanup import prune_orphaned_services
from server.indexer.pipeline import ProgressEvent
from server.state import get_reindex_lock
from server.store.commit_store import CommitStore
logger = logging.getLogger(__name__)
def _build_embedding_text(commit: GitHubCommit, service_name: str) -> str:
parts = [
f"Commit to {service_name} by {commit.author_name}",
f"Date: {commit.committed_at}",
"",
commit.message,
]
if commit.files:
file_list = ", ".join(f.filename for f in commit.files[:20])
parts.append(f"\nFiles changed: {file_list}")
return "\n".join(parts)
_MAX_FILES_IN_PAYLOAD = 50
_MAX_PATCH_CHARS = 2000
def _commit_to_payload(commit: GitHubCommit, service_name: str) -> dict[str, Any]:
files_payload = [
{
"filename": f.filename,
"status": f.status,
"additions": f.additions,
"deletions": f.deletions,
"patch": (f.patch or "")[:_MAX_PATCH_CHARS],
}
for f in commit.files[:_MAX_FILES_IN_PAYLOAD]
]
return {
"sha": commit.sha,
"service": service_name,
"message": commit.message,
"author_name": commit.author_name,
"author_email": commit.author_email,
"committed_at": commit.committed_at,
"indexed_at": datetime.now(timezone.utc).isoformat(),
"files": files_payload,
"has_diff": len(files_payload) > 0,
"diff_truncated": len(commit.files) > _MAX_FILES_IN_PAYLOAD,
}
class GitHistoryPipeline:
def __init__(self, store: CommitStore) -> None:
self._store = store
self._embedder: EmbeddingProvider = get_embedding_provider()
async def index_service(
self,
service_name: str,
force: bool = False,
progress_callback: Callable[[ProgressEvent], Awaitable[None]] | None = None,
) -> dict[str, int]:
await self._store.ensure_collection()
services = settings.load_services()
svc = next((s for s in services if s.name == service_name), None)
if svc is None:
return {"error": 1, "new": 0, "skipped": 0}
async with get_reindex_lock(f"history:{svc.name}"):
async with httpx.AsyncClient() as http_client:
commits = await list_commits(
settings.github_token,
svc.github_repo,
svc.github_ref,
root=svc.root,
max_commits=settings.git_history_max_commits,
client=http_client,
)
if progress_callback:
await progress_callback(
ProgressEvent(
phase="discovery",
current=len(commits),
total=len(commits),
percentage=100.0,
service=service_name,
)
)
existing_shas = (
set() if force else await self._store.get_indexed_shas(svc.name)
)
new_commits = [c for c in commits if c.sha not in existing_shas]
skipped = len(commits) - len(new_commits)
shas_without_diffs = (
set(await self._store.get_commits_without_diffs(svc.name))
if not force
else set()
)
commits_needing_diffs = [c for c in commits if c.sha in shas_without_diffs]
if not new_commits and not commits_needing_diffs:
return {"new": 0, "skipped": skipped, "diff_updated": 0}
diff_updated = 0
if new_commits:
logger.info(
"Fetching diffs for %d new commits in %s",
len(new_commits),
service_name,
)
new_commits = await fetch_commits_with_diffs(
settings.github_token, svc.github_repo, new_commits
)
texts = [_build_embedding_text(c, svc.name) for c in new_commits]
try:
vectors = await self._embedder.embed_batch(texts)
except Exception as exc:
logger.error(
"Embedding failed for %s git history: %s", service_name, exc
)
return {"error": 1, "new": 0, "skipped": skipped, "diff_updated": 0}
if progress_callback:
await progress_callback(
ProgressEvent(
phase="embedding",
current=len(new_commits),
total=len(new_commits),
percentage=100.0,
service=service_name,
)
)
payloads = [_commit_to_payload(c, svc.name) for c in new_commits]
await self._store.upsert_commits(svc.name, payloads, vectors)
logger.info(
"Indexed %d new commits for %s", len(new_commits), service_name
)
if progress_callback:
await progress_callback(
ProgressEvent(
phase="upserting",
current=len(new_commits),
total=len(new_commits),
percentage=100.0,
service=service_name,
)
)
if commits_needing_diffs:
logger.info(
"Fetching diffs for %d existing commits in %s",
len(commits_needing_diffs),
service_name,
)
commits_needing_diffs = await fetch_commits_with_diffs(
settings.github_token, svc.github_repo, commits_needing_diffs
)
payloads = [
_commit_to_payload(c, svc.name) for c in commits_needing_diffs
]
await self._store.update_commit_diffs(svc.name, payloads)
diff_updated = len(commits_needing_diffs)
if progress_callback:
await progress_callback(
ProgressEvent(
phase="upserting",
current=diff_updated,
total=diff_updated,
percentage=100.0,
service=service_name,
)
)
return {
"new": len(new_commits),
"skipped": skipped,
"diff_updated": diff_updated,
}
async def index_all(
self,
force: bool = False,
progress_callback: Callable[[ProgressEvent], Awaitable[None]] | None = None,
) -> dict[str, Any]:
services = settings.load_services()
await self._store.ensure_collection()
await prune_orphaned_services(
self._store, {s.name for s in services}, label="git commits"
)
results: dict[str, Any] = {}
for svc in services:
logger.info("Indexing git history for: %s", svc.name)
results[svc.name] = await self.index_service(
svc.name, force=force, progress_callback=progress_callback
)
return results