Ladybug 0.19 DROP INDEX leaves `_0_Leaf_vec_UPPER` / `0_id_docs` in catalog so CREATE fails while SHOW_INDEXES omits the index; create_fts_and_vector used to swallow that. Never drop FTS/VECTOR; ensure_indexes after upserts; rebuild = delete kb.lbug. Add compose.edelweiss.yml + regression tests. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.5 KiB
Python
Executable File
154 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""git/import - import git history (commits, authors, files) into the brain.
|
|
|
|
bin/git/import [REPO] import all commits -> leafs + graph
|
|
bin/git/import --json emit import leafs as JSON, no write
|
|
bin/git/import --limit 100 cap commits processed
|
|
bin/git/import --since 2026-01-01 only recent commits
|
|
bin/git/import --root DIR run per repo dir under DIR
|
|
bin/git/import --no-env never read .env anywhere (default: true)
|
|
|
|
Reads `git log --no-merges --name-only` from the repo, maps commits to
|
|
`info` leafs (root=info, type=commit) and writes the version graph
|
|
`File -[:HAS_VERSION]-> Commit -[:AUTHORED]-> Person` into var/kb.lbug.
|
|
Idempotent: leaf MERGE by (source,text via leaf_id), graph MERGE by sha.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "bin" / "tools"))
|
|
|
|
from kblib import ( # noqa: E402
|
|
connect, ensure_indexes, init_schema, upsert_leaf,
|
|
)
|
|
from gitimport import commits_to_leafs, ensure_git_schema, index_commits, parse_log # noqa: E402
|
|
|
|
LOG_FMT = "--format=%x1e%H%x1f%an%x1f%ae%x1f%aI%x1f%s"
|
|
|
|
|
|
def git_log(repo: Path, limit: int = 0, since: str = "") -> str:
|
|
cmd = ["git", "-C", str(repo), "log", "--no-merges", "--name-only", LOG_FMT]
|
|
if since:
|
|
cmd += ["--since", since]
|
|
if limit:
|
|
cmd += ["-n", str(limit)]
|
|
try:
|
|
out = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return ""
|
|
if out.returncode != 0:
|
|
print(f"git/import: {repo}: {out.stderr.strip()}", file=sys.stderr)
|
|
return ""
|
|
return out.stdout
|
|
|
|
|
|
def repo_name(repo: Path) -> str:
|
|
try:
|
|
out = subprocess.run(
|
|
["git", "-C", str(repo), "remote", "get-url", "origin"],
|
|
capture_output=True, text=True, timeout=20)
|
|
url = out.stdout.strip()
|
|
return url.rstrip("/").split("/")[-1].removesuffix(".git") if url else repo.name
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return repo.name
|
|
|
|
|
|
def embedder():
|
|
from model2vec import StaticModel
|
|
model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
|
|
return lambda text: model.encode([text])[0].astype(float).tolist()
|
|
|
|
|
|
def import_repo(conn, repo: Path, embed, limit: int, since: str,
|
|
no_write: bool = False) -> tuple[int, int]:
|
|
raw = git_log(repo, limit, since)
|
|
commits = parse_log(raw)
|
|
leafs = commits_to_leafs(commits, repo_name(repo))
|
|
if no_write:
|
|
return len(commits), 0
|
|
written = 0
|
|
for lf in leafs:
|
|
query = f"{lf['heading']}\n\n{lf['text']}"
|
|
emb = embed(lf["text"]) if lf["text"] else None
|
|
upsert_leaf(conn, text=query, root="info", confidence="confirmed",
|
|
source=lf["source"], source_rev="git", how="git/import",
|
|
loc=lf["source"], type_=lf.get("type", "commit"),
|
|
embedding=emb)
|
|
written += 1
|
|
index_commits(conn, commits, repo_name(repo))
|
|
return len(commits), written
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
import argparse
|
|
p = argparse.ArgumentParser(description="import git history into the brain")
|
|
p.add_argument("repo", nargs="?", default=None)
|
|
p.add_argument("--root", default=None, help="directory of repos to import (each git dir separately)")
|
|
p.add_argument("--limit", type=int, default=0)
|
|
p.add_argument("--since", default="")
|
|
p.add_argument("--json", action="store_true")
|
|
p.add_argument("--dry-run", action="store_true", help="parse + report, no db write")
|
|
a = p.parse_args(argv)
|
|
|
|
repos: list[Path] = []
|
|
if a.repo:
|
|
repos = [Path(a.repo)]
|
|
elif a.root:
|
|
root = Path(a.root)
|
|
if root.is_file():
|
|
repos = [root]
|
|
else:
|
|
repos = [dp for dp in sorted(root.iterdir()) if (dp / ".git").exists() or dp.is_file()]
|
|
else:
|
|
repos = [ROOT]
|
|
|
|
total_commits = 0
|
|
results: list[dict] = []
|
|
if a.dry_run:
|
|
for repo in repos:
|
|
if not repo.exists():
|
|
continue
|
|
commits = parse_log(git_log(repo, a.limit, a.since))
|
|
name = repo_name(repo)
|
|
total_commits += len(commits)
|
|
results.append({"repo": name, "commits": len(commits),
|
|
"leafs": len(commits_to_leafs(commits, name)), "path": str(repo)})
|
|
if a.json:
|
|
print(json.dumps(results, indent=2))
|
|
else:
|
|
for r in results:
|
|
print(f"{r['repo']:<24} {r['commits']:>5} commits -> {r['leafs']} leafs {r['path']}")
|
|
return 0
|
|
|
|
# Never DROP FTS/VECTOR (ghost catalog). Upsert while indexes exist is OK;
|
|
# ensure_indexes only CREATEs when missing.
|
|
db, conn = connect(ROOT / "var" / "kb.lbug", read_only=False)
|
|
init_schema(conn)
|
|
embed = embedder()
|
|
rows: list[dict] = []
|
|
for repo in repos:
|
|
if not repo.exists():
|
|
continue
|
|
reached, written = import_repo(conn, repo, embed, a.limit, a.since)
|
|
total_commits += reached
|
|
rows.append({"repo": repo_name(repo), "commits": reached, "written": written})
|
|
ensure_indexes(conn)
|
|
conn.close()
|
|
db.close()
|
|
|
|
if a.json:
|
|
print(json.dumps(rows, indent=2))
|
|
else:
|
|
for r in rows:
|
|
print(f"imported {r['commits']:>5} commits -> {r['written']} leafs {r['repo']}")
|
|
print(f"total: {total_commits} commits")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:])) |