File size: 5,679 Bytes
3df8a15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbb66cb
3df8a15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

 
import argparse
import json
import os
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
 
from github import Auth, Github
 
DATA_DIR = Path("data")
 
# Directories that add noise, not knowledge. Extend as you inspect results.
SKIP_DIRS = {".git", ".github", "__pycache__", "node_modules", "tests", "test",
             "scripts"}
CODE_EXTS = {".py"}
DOC_EXTS = {".md"}
MAX_FILE_BYTES = 400_000   # skip generated/vendored monsters
ISSUE_LOOKBACK_DAYS = 730  # ~2 years
 
 
def clone_repo(repo: str) -> Path:
    """Shallow-clone the repo into data/repo/ (we only need current state)."""
    dest = DATA_DIR / "repo"
    if dest.exists():
        print(f"[clone] {dest} already exists, skipping clone")
        return dest
    url = f"https://github.com/{repo}.git"
    print(f"[clone] {url} -> {dest}")
    subprocess.run(["git", "clone", "--depth", "1", url, str(dest)], check=True)
    return dest
 
 
def collect_files(repo_dir: Path) -> list[dict]:
    """Walk the clone and collect code + doc files worth indexing."""
    records = []
    for path in sorted(repo_dir.rglob("*")):
        if not path.is_file():
            continue
        rel = path.relative_to(repo_dir)
        if any(part in SKIP_DIRS for part in rel.parts):
            continue
        # FastAPI-specific: keep only English docs, skip tutorial snippets
        if rel.parts[0] == "docs" and rel.parts[1] != "en":
            continue
        if rel.parts[0] == "docs_src":
            continue
        ext = path.suffix.lower()
        if ext in CODE_EXTS:
            source_type = "code"
        elif ext in DOC_EXTS:
            source_type = "doc"
        else:
            continue
        size = path.stat().st_size
        if size == 0 or size > MAX_FILE_BYTES:
            continue
        records.append({
            "source_type": source_type,
            "path": str(rel),
            "size_bytes": size,
        })
    return records
 
 
def fetch_issues(repo: str, max_issues: int) -> list[dict]:
    """Pull recent closed issues (not PRs) and save each as JSON.
 
    Resumable: issues already saved to disk are skipped (no API calls),
    so a crashed or interrupted run can simply be re-run.
    """
    token = os.environ.get("GITHUB_TOKEN")
    if not token:
        print("[warn] no GITHUB_TOKEN set - unauthenticated limit is 60 req/hr")
    gh = Github(auth=Auth.Token(token)) if token else Github()
    since = datetime.now(timezone.utc) - timedelta(days=ISSUE_LOOKBACK_DAYS)
    issues_dir = DATA_DIR / "issues"
    issues_dir.mkdir(parents=True, exist_ok=True)
 
    records = []
    # sort="comments" surfaces the most-discussed (usually most useful) issues first
    issues = gh.get_repo(repo).get_issues(state="closed", since=since,
                                          sort="comments", direction="desc")
    for issue in issues:
        if issue.pull_request is not None:  # PRs come through the same API; skip
            continue
        if len(records) >= max_issues:
            break
 
        # Resume support: skip issues already saved from a previous run
        out_path = issues_dir / f"{issue.number}.json"
        if out_path.exists():
            records.append({"source_type": "issue",
                            "path": f"issues/{issue.number}.json",
                            "url": issue.html_url})
            continue
 
        # Keep up to 3 comments, favoring the most 👍-reacted (often the answer)
        comments = []
        if issue.comments > 0:
            all_comments = list(issue.get_comments())
            all_comments.sort(key=lambda c: c.reactions.get("+1", 0), reverse=True)
            comments = [c.body for c in all_comments[:3] if c.body]
 
        record = {
            "number": issue.number,
            "title": issue.title,
            "body": issue.body or "",
            "labels": [label.name for label in issue.labels],
            "url": issue.html_url,
            "comments": comments,
        }
        out_path.write_text(
            json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
        records.append({"source_type": "issue",
                        "path": f"issues/{issue.number}.json",
                        "url": issue.html_url})
        if len(records) % 50 == 0:
            print(f"[issues] fetched {len(records)}...")
    return records
 
 
def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--repo", required=True, help="e.g. fastapi/fastapi")
    parser.add_argument("--max-issues", type=int, default=300)
    parser.add_argument("--skip-issues", action="store_true",
                        help="only clone + collect files (useful while iterating)")
    args = parser.parse_args()
 
    DATA_DIR.mkdir(exist_ok=True)
    repo_dir = clone_repo(args.repo)
    file_records = collect_files(repo_dir)
    issue_records = [] if args.skip_issues else fetch_issues(args.repo,
                                                             args.max_issues)
 
    manifest = {
        "repo": args.repo,
        "ingested_at": datetime.now(timezone.utc).isoformat(),
        "files": file_records,
        "issues": issue_records,
    }
    (DATA_DIR / "manifest.json").write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
 
    n_code = sum(1 for r in file_records if r["source_type"] == "code")
    n_docs = sum(1 for r in file_records if r["source_type"] == "doc")
    print(f"\n[done] {n_code} code files, {n_docs} docs, {len(issue_records)} issues")
    print(f"[done] manifest -> {DATA_DIR / 'manifest.json'}")
 
 
if __name__ == "__main__":
    main()