Skip to content

Commit f74e9bb

Browse files
committed
wip:RAG pipeline:general structure #6 - ingestion: extract git change discovery
1 parent 2cac5cc commit f74e9bb

2 files changed

Lines changed: 83 additions & 18 deletions

File tree

ingestion/git_changes.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Find repository files changed since the last successful ingestion.
2+
3+
This module reads git history as a recovery cursor: it narrows the scan to
4+
files touched since ``last_ingestion`` and reports old paths that disappeared
5+
so their vector-store chunks can be deleted. It does not decide whether a
6+
current file is truly changed; the pipeline still uses ``file_sha1`` for that
7+
state-based decision.
8+
"""
9+
10+
import subprocess
11+
from pathlib import Path
12+
13+
14+
# Build repo -> repo-relative current candidates and stale paths from git
15+
# history. None candidates means "first run, full scan"; stale paths are files
16+
# deleted or renamed away since the last successful ingestion.
17+
def candidate_file_changes(repo_dirs, last_ingestion):
18+
if not last_ingestion:
19+
return None, {}
20+
candidates = {}
21+
stale = {}
22+
for repo_dir in repo_dirs:
23+
changed, removed = _file_changes_since(repo_dir, last_ingestion)
24+
if changed:
25+
candidates[Path(repo_dir).name] = changed
26+
if removed:
27+
stale[Path(repo_dir).name] = removed
28+
return candidates, stale
29+
30+
31+
# Repo-relative file paths touched since `since`, split into current paths to
32+
# re-check and stale paths to delete from the vector store.
33+
def _file_changes_since(repo_dir, since):
34+
try:
35+
result = subprocess.run(
36+
["git", "-C", str(repo_dir), "log", f"--since={since}",
37+
"--name-status", "--pretty=format:"],
38+
capture_output=True, text=True, check=False, timeout=15)
39+
except (FileNotFoundError, subprocess.TimeoutExpired):
40+
return set(), set()
41+
if result.returncode != 0:
42+
return set(), set()
43+
changed = set()
44+
stale = set()
45+
for line in result.stdout.splitlines():
46+
_collect_name_status(line, changed, stale)
47+
return changed, stale
48+
49+
50+
# Interpret one git --name-status line. Add current file paths to `changed`
51+
# and removed/old paths to `stale`.
52+
def _collect_name_status(line, changed, stale):
53+
parts = [part.strip().replace("\\", "/")
54+
for part in line.split("\t") if part.strip()]
55+
if not parts:
56+
return
57+
if len(parts) == 1:
58+
changed.add(parts[0])
59+
return
60+
status = parts[0]
61+
code = status[0]
62+
if code == "D" and len(parts) >= 2:
63+
stale.add(parts[1])
64+
elif code in {"R", "C"} and len(parts) >= 3:
65+
if code == "R":
66+
stale.add(parts[1])
67+
changed.add(parts[2])
68+
elif len(parts) >= 2:
69+
changed.add(parts[1])

ingestion/main.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from pathlib import Path
2525

2626
from ingestion.config import IngestionConfig
27+
from ingestion import git_changes
2728

2829

2930
# Directories under the workspace root that are not part of the indexed
@@ -106,12 +107,14 @@ def main(argv=None):
106107

107108
repo_dirs = _ensure_repos(root, config, argv)
108109
last_ingestion = vectordb.get_last_ingestion()
109-
candidate_files = _candidate_files(repo_dirs, last_ingestion)
110+
candidate_files, stale_files = git_changes.candidate_file_changes(
111+
repo_dirs, last_ingestion)
110112

111113
print(f"indexing {len(repo_dirs)} repo(s): "
112114
f"{', '.join(d.name for d in repo_dirs)}")
113115
if last_ingestion:
114116
print(f"last successful ingestion: {last_ingestion}")
117+
_delete_stale_files(stale_files, vectordb)
115118
count = run(repo_dirs, candidate_files=candidate_files)
116119
vectordb.set_last_ingestion(_utc_now_iso8601())
117120
print(f"indexed {count} chunks into '{config.qdrant_collection}'")
@@ -129,9 +132,6 @@ def main(argv=None):
129132
def _ensure_repos(root, config, names=None):
130133
root.mkdir(parents=True, exist_ok=True)
131134
_ensure_development(root, config.development_repo_url)
132-
repo_dirs = _discover_repos(root, names)
133-
if repo_dirs:
134-
return repo_dirs
135135
_run_k_clone(root, config.kli_organization, config.kli_workspace)
136136
repo_dirs = _discover_repos(root, names)
137137
if not repo_dirs:
@@ -190,23 +190,19 @@ def _candidate_files(repo_dirs, last_ingestion):
190190
return candidates
191191

192192

193+
# Delete vector-store chunks for files that disappeared from git history
194+
# (deleted files, or the old side of renames).
195+
def _delete_stale_files(stale_files, vectordb):
196+
for repository, source_paths in stale_files.items():
197+
for source_path in source_paths:
198+
vectordb.delete_file(repository, source_path)
199+
200+
193201
# Repo-relative file paths touched since `since` (ISO 8601), newest activity
194202
# first in git history but returned here as a deduplicated set.
195203
def _git_changed_files_since(repo_dir, since):
196-
try:
197-
result = subprocess.run(
198-
["git", "-C", str(repo_dir), "log", f"--since={since}",
199-
"--name-only", "--pretty=format:"],
200-
capture_output=True, text=True, check=False, timeout=15)
201-
except (FileNotFoundError, subprocess.TimeoutExpired):
202-
return set()
203-
if result.returncode != 0:
204-
return set()
205-
return {
206-
line.strip().replace("\\", "/")
207-
for line in result.stdout.splitlines()
208-
if line.strip()
209-
}
204+
candidates, _ = git_changes.candidate_file_changes([repo_dir], since)
205+
return candidates.get(Path(repo_dir).name, set())
210206

211207

212208
# Current UTC timestamp in compact ISO 8601 form used by the metadata

0 commit comments

Comments
 (0)