Spaces:
Sleeping
Sleeping
File size: 5,906 Bytes
910dadd | 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 | """Build data/prs.jsonl from oumi-ai/oumi git history.
Mines squash-merge commits (subject ending in "(#NNNN)"), derives a subsystem
area label from the files each PR changed, and (best-effort) fetches PR bodies
and merge dates via the GitHub GraphQL API using `gh` auth.
Usage:
git clone --filter=blob:none --single-branch https://github.com/oumi-ai/oumi /tmp/oumi-mine
uv run python scripts/build_pr_dataset.py --repo /tmp/oumi-mine [--no-bodies]
"""
import argparse
import json
import re
import subprocess
from collections import Counter
from pathlib import Path
OUT_PATH = Path(__file__).resolve().parent.parent / "data" / "prs.jsonl"
AREAS = [
"training", "inference", "data", "evaluation", "configs",
"launcher", "cli", "docs", "infra", "other",
]
# Ordered (prefix-or-regex, area) rules; first match wins. Paths not matching
# any rule fall through to "other". Tests and barrel files don't vote.
IGNORE = re.compile(r"^tests/|(^|/)__init__\.py$")
RULES: list[tuple[re.Pattern, str]] = [
(re.compile(r"^docs/|^notebooks/|\.(md|rst|ipynb)$|^CITATION|^LICENSE"), "docs"),
(re.compile(r"^\.github/|^scripts/|^pyproject\.toml$|^uv\.lock$|^Makefile$|^Dockerfile|^install\.sh$|^\.pre-commit|^\.(gitignore|dockerignore)|^\.style|^\.readthedocs"), "infra"),
(re.compile(r"^configs/|^src/(oumi|lema)/core/configs/"), "configs"),
(re.compile(r"^src/(oumi|lema)/(inference/|infer.*\.py$|deploy/)|^src/(oumi|lema)/core/inference/"), "inference"),
(re.compile(r"^src/(oumi|lema)/core/(trainers|tuners|callbacks|collators)/|^src/(oumi|lema)/(train|tune)\.py$|^src/(oumi|lema)/performance/|^src/(oumi|lema)/core/distributed\.py$"), "training"),
(re.compile(r"^src/(oumi|lema)/(evaluation/|evaluate.*\.py$|judges/|judge\.py$|analyze/)|^src/(oumi|lema)/core/(evaluation|analyze)/"), "evaluation"),
(re.compile(r"^src/(oumi|lema)/(datasets/|synth\.py$)|^src/(oumi|lema)/core/(datasets|tokenizers|processors|feature_generators|synthesis|types)/|^data/"), "data"),
(re.compile(r"^src/(oumi|lema)/launcher/|^src/(oumi|lema)/core/launcher/"), "launcher"),
(re.compile(r"^src/(oumi|lema)/cli/"), "cli"),
]
SUBJECT_RE = re.compile(r"^(?P<title>.*)\s+\(#(?P<number>\d+)\)$")
DOMINANCE = 0.70
MAX_BODY_CHARS = 4000
def map_file(path: str) -> str | None:
if IGNORE.search(path):
return None
for pattern, area in RULES:
if pattern.search(path):
return area
return "other"
def derive_area(files: list[str]) -> tuple[str | None, dict[str, int]]:
"""Return (area, votes); area is None when no file dominates."""
votes = Counter(a for f in files if (a := map_file(f)))
if not votes:
return None, {}
area, top = votes.most_common(1)[0]
if top / sum(votes.values()) < DOMINANCE:
return None, dict(votes)
return area, dict(votes)
def mine_commits(repo: str) -> list[dict]:
# Single pass: NUL-prefixed header line per commit, then its changed files.
log = subprocess.run(
["git", "-C", repo, "log", "--name-only", "--no-merges",
"--format=%x00%s", "main"],
capture_output=True, text=True, check=True,
).stdout
records = []
for block in log.split("\x00"):
if not block.strip():
continue
subject, _, file_block = block.partition("\n")
m = SUBJECT_RE.match(subject.strip())
if not m:
continue
files = [ln.strip() for ln in file_block.splitlines() if ln.strip()]
area, votes = derive_area(files)
if area is None:
continue
records.append({
"number": int(m["number"]),
"title": m["title"].strip(),
"files": files,
"area": area,
"area_votes": votes,
})
# Dedupe (reverts etc. can repeat a PR number); keep first (newest) occurrence.
seen: set[int] = set()
return [r for r in records if not (r["number"] in seen or seen.add(r["number"]))]
def fetch_bodies(records: list[dict], batch_size: int = 50) -> None:
"""Attach `body` and `merged_at` in-place via GitHub GraphQL; skip on failure."""
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
fields = "".join(
f'p{r["number"]}: pullRequest(number: {r["number"]}) {{ body mergedAt }}\n'
for r in batch
)
query = f'query {{ repository(owner: "oumi-ai", name: "oumi") {{ {fields} }} }}'
try:
out = subprocess.run(
["gh", "api", "graphql", "-f", f"query={query}"],
capture_output=True, text=True, check=True, timeout=120,
).stdout
repo_data = json.loads(out)["data"]["repository"]
except (subprocess.SubprocessError, KeyError, json.JSONDecodeError) as e:
print(f" body fetch failed for batch at {i}: {e}; continuing without")
continue
for r in batch:
pr = repo_data.get(f'p{r["number"]}') or {}
r["body"] = (pr.get("body") or "")[:MAX_BODY_CHARS]
r["merged_at"] = pr.get("mergedAt")
print(f" bodies {i + len(batch)}/{len(records)}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default="/tmp/oumi-mine")
parser.add_argument("--no-bodies", action="store_true")
args = parser.parse_args()
records = mine_commits(args.repo)
print(f"mined {len(records)} PRs with unambiguous areas")
print("label distribution:", Counter(r["area"] for r in records).most_common())
if not args.no_bodies:
fetch_bodies(records)
records.sort(key=lambda r: -r["number"])
OUT_PATH.parent.mkdir(exist_ok=True)
with OUT_PATH.open("w") as f:
for r in records:
f.write(json.dumps(r) + "\n")
print(f"wrote {len(records)} records to {OUT_PATH}")
if __name__ == "__main__":
main()
|