tkgl-toybox's picture
agent-overwrite-grader 0.1.0
a8e4869 verified
Raw History Blame Contribute Delete
3.52 kB
"""The grading rule: which files two sessions wrote within an hour, read from a git log.
A line-for-line port of the web grader's grade.js; a shared fixture test keeps the two verdicts equal.
"""
import math
import re
from datetime import datetime
COMMAND = "git log --all --source --name-only --since='1 month ago' --format='%H|%an|%aI|%S'"
WINDOW_MIN = 60
HEAD = re.compile(r"^[0-9a-f]{7,64}\|")
def branch(ref):
"""A ref as git prints it (%S, or %D's decorations) to a branch name; "" when the commit carries none."""
names = [s.strip() for s in (ref or "").split(",")]
names = [re.sub(r"^HEAD -> ", "", s) for s in names]
names = [s for s in names if s and s != "HEAD" and not s.startswith("tag: ") and not s.startswith("refs/tags/")]
if not names:
return ""
return re.sub(r"^refs/remotes/[^/]+/", "", re.sub(r"^refs/heads/", "", names[0]))
def millis(text):
"""An ISO 8601 date with its offset to epoch milliseconds; None when it does not read as one."""
try:
when = datetime.fromisoformat(text.strip().replace("Z", "+00:00"))
except ValueError:
return None
return int(round(when.timestamp() * 1000))
def parse(text):
commits, cur = [], None
for line in re.split(r"\r?\n", str(text or "")):
if HEAD.match(line):
p = line.split("|")
when = millis(p[-2]) if len(p) >= 4 else None
if when is not None:
cur = {"hash": p[0], "author": "|".join(p[1:-2]), "when": when, "ref": branch(p[-1]), "files": []}
commits.append(cur)
continue
f = line.strip()
if cur is not None and f:
cur["files"].append(f)
return commits
def two_sessions(a, b):
"""Two commits are two sessions when their authors differ, or both carry a branch and the branches differ."""
return a["author"] != b["author"] or bool(a["ref"] and b["ref"] and a["ref"] != b["ref"])
def grade(text):
commits, by_file = parse(text), {}
for c in commits:
for f in c["files"]:
by_file.setdefault(f, []).append(c)
files = []
for name, writes in by_file.items():
writes = sorted(writes, key=lambda c: c["when"])
pairs, latest = [], 0
for i in range(1, len(writes)):
j = i - 1
while j >= 0 and writes[i]["when"] - writes[j]["when"] <= WINDOW_MIN * 60000:
if two_sessions(writes[i], writes[j]):
minutes = int(math.floor((writes[i]["when"] - writes[j]["when"]) / 60000 + 0.5))
pairs.append({"a": writes[j], "b": writes[i], "minutes": minutes})
latest = max(latest, writes[i]["when"])
j -= 1
if pairs:
pairs.sort(key=lambda p: p["minutes"])
files.append({"file": name, "count": len(pairs), "latest": latest, "pairs": pairs[:3]})
files.sort(key=lambda f: (-f["count"], -f["latest"], f["file"]))
times = [c["when"] for c in commits]
days = max(1, math.ceil((max(times) - min(times)) / 864e5)) if times else 0
sessions = {(c["author"], c["ref"]) for c in commits}
return {"commits": len(commits), "sessions": len(sessions), "days": days, "files": files}
def score_line(report):
n = len(report["files"])
head = "No file" if n == 0 else "1 file" if n == 1 else "%d files" % n
return "%s written by two sessions within an hour, in %d %s of history." % (
head, report["days"], "day" if report["days"] == 1 else "days")