File size: 5,660 Bytes
f10d14f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Maintainer study (eval/maintainer_notes_rubric.json): reviewers' notes on the public replay's merge requests in
GitLab's own codebase, filtered by a fixed word list, packed for two blind labellers. Everything comes from the replay's
cache (eval/cache/gitlab/); packets hold note text, so they are written OUTSIDE the repository (--out).

usage: python3 eval/maintainer_notes.py --out <private dir>        (frame counts go to results/maintainer_notes_frame.json)"""
from __future__ import annotations

import argparse, json, pathlib, random, re, sys

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
import replay_gitlab as G

SCOPE = re.compile(r"(?i)\b(?:remov(?:e|ed|ing)|revert(?:ed|ing)?|unrelated|out[- ]of[- ]scope|scope|separate (?:mr|merge request)|"
                   r"split|not (?:needed|necessary|required)|unnecessary|follow[- ]?up|why (?:is|was|are|were|do|did) (?:this|these|we|you))\b")
FRAME = G.ROOT / "results" / "maintainer_notes_frame.json"
CONTROL, SEED = 150, 20261018


def reviewer_notes(mr: dict, notes: list) -> list:
    """Notes by someone other than the merge request's author: no system notes, bots, duo- accounts or GitLabDuo."""
    author = (mr.get("author") or {}).get("username")
    def reviewer(n):
        u = (n.get("author") or {}).get("username") or ""
        return u != author and not G.is_bot(u) and not u.startswith("duo-") and u != "GitLabDuo"
    return [n for n in notes if not n.get("system") and (n.get("body") or "").strip() and reviewer(n)]


def note_path(n: dict) -> str | None:
    return (n.get("position") or {}).get("new_path") if n.get("type") == "DiffNote" else None


def rows_of(raw_a1: list, raw_a2: list) -> list:
    """The scored merge requests: G on its agent version, H and C on their merged version."""
    keep = lambda r, pop, version: r["pop"] == pop and r["version"] == version and not r.get("excluded")
    return [r for r in raw_a1 if keep(r, "G", "agent") or keep(r, "H", "merged")] + [r for r in raw_a2 if keep(r, "C", "merged")]


def frame(rows: list, mr_of, notes_of) -> tuple[list, dict]:
    """Every reviewer note of every row, marked as matching SCOPE or not, plus the counts."""
    out, counts = [], {}
    for r in rows:
        mr = mr_of(r)
        ns = reviewer_notes(mr, notes_of(r))
        c = counts.setdefault(r["pop"], {"merge_requests": 0, "reviewer_notes": 0, "candidates": 0})
        c["merge_requests"] += 1
        c["reviewer_notes"] += len(ns)
        for n in ns:
            hit = bool(SCOPE.search(n["body"]))
            c["candidates"] += hit
            out.append({"pop": r["pop"], "ref": r["ref"], "note_id": n["id"], "path": note_path(n), "candidate": hit})
    return out, counts


def control(notes: list, n: int = CONTROL, seed: int = SEED) -> list:
    rest = sorted((x for x in notes if not x["candidate"]), key=lambda x: x["note_id"])
    return rest if len(rest) <= n else sorted(random.Random(seed).sample(rest, n), key=lambda x: x["note_id"])


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(); ap.add_argument("--out", required=True)
    a = ap.parse_args(argv)
    raw1 = json.loads((G.ROOT / "results" / "public_replay_a1_raw.json").read_text())
    raw2 = json.loads((G.ROOT / "results" / "public_replay_a2_raw.json").read_text())
    rows = rows_of(raw1, raw2)
    iid = lambda r: int(r["ref"].split("!")[1])
    mrs = {r["ref"]: G.get(f"projects/{G.GITLAB}/merge_requests/{iid(r)}") or {} for r in rows}
    notes = {r["ref"]: G.notes_of(G.GITLAB, iid(r)) for r in rows}
    allnotes, counts = frame(rows, lambda r: mrs[r["ref"]], lambda r: notes[r["ref"]])
    chosen = [x for x in allnotes if x["candidate"]] + [dict(x, control=True) for x in control(allnotes)]
    out = pathlib.Path(a.out); out.mkdir(parents=True, exist_ok=True)
    by_id = {n["id"]: n for ns in notes.values() for n in ns}
    row_of = {r["ref"]: r for r in rows}
    names = []
    for x in chosen:
        mr, r = mrs[x["ref"]], row_of[x["ref"]]
        n = int(r["issue_refs"][0].lstrip("#")) if r.get("issue_refs") else None
        issue = (G.get(f"projects/{G.GITLAB}/issues/{n}") or {}) if n else {}
        name = f"{x['pop']}__{x['ref'].split('!')[1]}__{x['note_id']}.json"
        (out / name).write_text(json.dumps({"ref": x["ref"], "note_id": x["note_id"], "path": x["path"],
                                            "note": by_id[x["note_id"]]["body"][:3000], "mr_title": mr.get("title") or "",
                                            "mr_description": (mr.get("description") or "")[:4000],
                                            "issue": f"{issue.get('title') or ''}\n{(issue.get('description') or '')[:4000]}"},
                                           indent=1, ensure_ascii=False) + "\n")
        names.append(name)
    rubric = json.loads((G.ROOT / "eval" / "maintainer_notes_rubric.json").read_text())
    (out / "INDEX.json").write_text(json.dumps({"rubric": {k: rubric[k] for k in ("labels", "file")}, "packets": names}, indent=1) + "\n")
    FRAME.write_text(json.dumps({"what": "Maintainer study frame (eval/maintainer_notes_rubric.json): counts only.", "counts": counts,
                                 "control": len(chosen) - sum(x["candidate"] for x in chosen),
                                 "notes": sorted(({"ref": x["ref"], "note_id": x["note_id"], "path": x["path"], "candidate": x["candidate"],
                                                   "control": bool(x.get("control"))} for x in chosen), key=lambda x: x["note_id"])},
                                indent=1) + "\n")
    print(counts, len(chosen))
    return 0


if __name__ == "__main__":
    sys.exit(main())