Spaces:
Running
Running
File size: 12,743 Bytes
cc857c8 | 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 | import json, subprocess, html as H, re
from collections import Counter, defaultdict
from datetime import datetime, timezone
NOW = datetime(2026, 10, 21, tzinfo=timezone.utc)
days = lambda s: (NOW - datetime.fromisoformat(s.replace('Z', '+00:00'))).days
STAFF = {'gante','Narsil','qubvel','kashif','XciD','asartran','ydshieh','amyeroberts','ArthurZucker','Rocketknight1','zucchini-nlp','stevhliu','SunMarc','MekkCyber','vasqu','molbap','Cyrilvallez','McPatate','eustlb','yonigozlan','LysandreJik','NielsRogge','itazap','remi-or','3outeille','IlyasMoutawwakil','ivarflakstad','manueldeprada','ebezzam','guarin','danieldk','drbh','merveenoyan','BenjaminBossan','qgallouedec','tarekziade','Wauplin','hanouticelina'}
CORE = {'ArthurZucker', 'Cyrilvallez', 'LysandreJik', 'vasqu'}
HFA = {'MEMBER', 'OWNER', 'COLLABORATOR'}
isstaff = lambda login, assoc: bool(login) and (login in STAFF or assoc in HFA) and not re.search(r'(?i)bot', login)
D = {int(k): v for k, v in json.load(open('rq.json')).items()}
G = ['git', '-C', '/Users/arthurzucker/Work/transformers']
wait, appr = [], []
for n, p in D.items():
au = (p['author'] or {}).get('login') or ''
revs = [r for r in p['reviews']['nodes'] if r['author'] and r['author']['login'] != au]
srevs = [r for r in revs if isstaff(r['author']['login'], r['authorAssociation'])]
scom = [c for c in p['comments']['nodes'] if c['author'] and c['author']['login'] != au and isstaff(c['author']['login'], c['authorAssociation']) and not (c['author']['login'] == 'ArthurZucker' and c['createdAt'] >= '2026-10-20')] # ponytail: today's triage pings aren't reviews
acom = [c['createdAt'] for c in p['comments']['nodes'] if c['author'] and c['author']['login'] == au]
commit = p['commits']['nodes'][0]['commit'] if p['commits']['nodes'] else {}
ci = ((commit.get('statusCheckRollup') or {}).get('state')) or 'none'
last_author = max([commit.get('committedDate') or p['createdAt']] + acom)
last_staff = max([r['submittedAt'] for r in srevs if r['submittedAt']] + [c['createdAt'] for c in scom] or [''])
ready = max([e['createdAt'] for e in p['timelineItems']['nodes'] if e['__typename'] == 'ReadyForReviewEvent'] or [p['createdAt']])
reqs = [(r['requestedReviewer'] or {}).get('login') or (r['requestedReviewer'] or {}).get('name') for r in p['reviewRequests']['nodes']]
reqs = [r for r in reqs if r]
req_at = {e['requestedReviewer']['login']: e['createdAt'] for e in p['timelineItems']['nodes'] if e['__typename'] == 'ReviewRequestedEvent' and e.get('requestedReviewer')}
conflict = p['mergeable'] == 'CONFLICTING'
flags = (['conflicts'] if conflict else []) + (['CI red'] if ci in ('FAILURE', 'ERROR') else [])
base = dict(n=n, t=p['title'], au=au, hf=isstaff(au, p['authorAssociation']), ci=ci, flags=flags, age=days(p['createdAt']))
approvals = [r for r in srevs if r['state'] == 'APPROVED']
if p['reviewDecision'] == 'APPROVED' or approvals:
a = max(approvals, key=lambda r: r['submittedAt']) if approvals else None
when = a['submittedAt'] if a else p['updatedAt']; who = a['author']['login'] if a else '?'
core_ok = any(r['author']['login'] in CORE for r in approvals)
after = (commit.get('committedDate') or '') > when
files = [f['path'] for f in p['files']['nodes']]
churn = int(subprocess.run(G + ['rev-list', '--count', f'--since={when}', 'origin/main', '--'] + files, capture_output=True, text=True).stdout.strip() or 0) if files else 0
gone = sum(1 for f in files if subprocess.run(G + ['cat-file', '-e', f'origin/main:{f}'], capture_output=True).returncode) if files else 0
why = []
if conflict: why.append('needs rebase (conflicts)')
if ci in ('FAILURE', 'ERROR'): why.append('CI red')
if after: why.append('pushed after approval → needs re-approval')
if not core_ok: why.append('no core-maintainer approval yet' + (f' (approved by @{who})' if who != '?' else ''))
if gone and gone >= len(files) / 2: why.append(f'likely superseded/outdated: {gone}/{len(files)} files gone from main')
elif churn >= 15: why.append(f'likely outdated: main touched its files {churn}× since approval')
if not why: why.append('nothing blocking → probably forgotten, ready to merge')
if p['baseRefName'] != 'main': why.insert(0, f"targets `{p['baseRefName']}`, not main")
appr.append({**base, 'who': who, 'when': when[:10], 'd': days(when), 'why': why, 'primary': why[0].split(' (')[0].split(':')[0], 'churn': churn})
continue
if p['reviewDecision'] == 'CHANGES_REQUESTED' or (last_staff and last_staff > last_author):
since = last_staff; who_last = (max(srevs + [{'submittedAt': c['createdAt'], 'author': c['author']} for c in scom], key=lambda r: r['submittedAt'] or '')['author']['login']) if (srevs or scom) else '?'
wait.append({**base, 'bucket': 'waiting on author', 'since': since[:10], 'd': days(since), 'why': f'@{who_last} replied last ({since[:10]}); ball is with the author', 'who': who_last}); continue
if not srevs and not scom:
since = ready
if reqs:
r0 = reqs[0]; ra = req_at.get(r0, since)
why = f'never reviewed · review requested from @{", @".join(reqs)} {days(ra)}d ago, no response'; bucket = 'assigned, no response'
else:
why = 'never reviewed · no reviewer assigned'; bucket = 'no reviewer assigned'
else:
since = last_author
who_last = max(srevs + [{'submittedAt': c['createdAt'], 'author': c['author']} for c in scom], key=lambda r: r['submittedAt'] or '')['author']['login']
why = f'author updated after @{who_last}\'s review → needs re-review' + (f' (requested: @{", @".join(reqs)})' if reqs else ''); bucket = 'needs re-review'
if base['hf']: bucket = 'HF-authored, no review'; why = 'maintainer PR · ' + why
wait.append({**base, 'bucket': bucket, 'since': since[:10], 'd': days(since), 'why': why, 'who': ', '.join(reqs)})
QA = json.load(open('qa_verdicts.json'))
def tag(r):
q = QA.get(str(r['n']))
if not q: return ''
return '✓ audited: still valid' if q['verdict'] == 'valid' else f"⚠ audit: {q['verdict']}" + (f" (#{q['by']})" if q.get('by') else '')
for r in appr: r['why'] = r['why'] + ([tag(r)] if tag(r) else [])
for r in wait:
if tag(r): r['why'] = r['why'] + ' · ' + tag(r)
appr = [r for r in appr if QA.get(str(r['n']), {}).get('state', 'OPEN') == 'OPEN']
wait = [r for r in wait if QA.get(str(r['n']), {}).get('state', 'OPEN') == 'OPEN']
json.dump({'wait': wait, 'appr': appr}, open('rq_rows.json', 'w'))
DB = lambda d: '<7d' if d < 7 else '7–30d' if d < 30 else '30–90d' if d < 90 else '90–180d' if d < 180 else '>180d'
DUR = ['<7d', '7–30d', '30–90d', '90–180d', '>180d']
def matrix(rows, keyf, keys, title):
m = Counter((keyf(r), DB(r['d'])) for r in rows); tot = Counter(keyf(r) for r in rows)
head = ''.join(f'<th>{d}</th>' for d in DUR)
body = ''.join(f"<tr><td class='l'>{H.escape(k)}</td>" + ''.join(f"<td class='c' style='--a:{min(1, m[(k, d)] / 25):.2f}' title='{H.escape(k)} · {d}: {m[(k, d)]}'>{m[(k, d)] or ''}</td>" for d in DUR) + f"<td><b>{tot[k]}</b></td></tr>" for k in keys if tot[k])
return f"<section class='card'><h3>{title}</h3><table class='mx'><tr><th></th>{head}<th>total</th></tr>{body}</table></section>"
def table(rows, cols, sid):
th = ''.join(f'<th>{c}</th>' for c, _ in cols)
tr = ''.join('<tr>' + ''.join(f'<td>{f(r)}</td>' for _, f in cols) + '</tr>' for r in rows)
return f"<table class='list' id='{sid}'><thead><tr>{th}</tr></thead><tbody>{tr}</tbody></table>"
link = lambda r: f"<a href='https://github.com/huggingface/transformers/pull/{r['n']}' target='_blank'>#{r['n']}</a>"
esc = lambda s: H.escape(str(s))
WB = ['no reviewer assigned', 'assigned, no response', 'needs re-review', 'HF-authored, no review', 'waiting on author']
ours = [r for r in wait if r['bucket'] != 'waiting on author']
wsec = ''
for b in WB:
rs = sorted([r for r in wait if r['bucket'] == b], key=lambda r: -r['d'])
if not rs: continue
wsec += f"<details {'open' if b != 'waiting on author' else ''}><summary><b>{b}</b> — {len(rs)} PRs · median wait {sorted(r['d'] for r in rs)[len(rs)//2]}d</summary>" + table(rs, [('PR', link), ('title', lambda r: esc(r['t'][:90])), ('author', lambda r: esc('@' + r['au'])), ('waiting', lambda r: f"{r['d']}d <span class='mut'>since {r['since']}</span>"), ('why', lambda r: esc(r['why'])), ('flags', lambda r: esc(', '.join(r['flags'])))], 'w' + b[:4]) + '</details>'
AB = ['nothing blocking → probably forgotten, ready to merge', 'no core-maintainer approval yet', 'pushed after approval → needs re-approval', 'needs rebase', 'CI red', 'likely outdated', 'likely superseded/outdated', 'targets `']
asec = ''
for b in sorted({r['primary'] for r in appr}, key=lambda x: (0 if x.startswith('nothing') else 1, x)):
rs = sorted([r for r in appr if r['primary'] == b], key=lambda r: -r['d'])
asec += f"<details open><summary><b>{esc(b)}</b> — {len(rs)} PRs</summary>" + table(rs, [('PR', link), ('title', lambda r: esc(r['t'][:90])), ('author', lambda r: esc('@' + r['au'])), ('approved', lambda r: f"{r['when']} by @{esc(r['who'])} <span class='mut'>({r['d']}d ago)</span>"), ('why not merged', lambda r: '<br>'.join(esc(w) for w in r['why']))], 'a' + str(len(asec))) + '</details>'
tiles = (f"<div class='tiles'><div class='tile'><b>{len(ours)}</b>waiting on a maintainer</div><div class='tile'><b>{sum(r['d']>=90 for r in ours)}</b>of those waiting ≥ 90 days</div>"
f"<div class='tile'><b>{sum(r['bucket']=='no reviewer assigned' for r in wait)}</b>never reviewed, nobody assigned</div><div class='tile'><b>{len(appr)}</b>approved, not merged</div>"
f"<div class='tile'><b>{sum(r['primary'].startswith('nothing') for r in appr)}</b>approved & nothing blocking</div><div class='tile'><b>{sum(r['bucket']=='waiting on author' for r in wait)}</b>waiting on the author</div></div>")
page = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Review Queue</title><style>
:root{{--bg:#fafaf9;--fg:#1c1917;--mut:#78716c;--card:#fff;--bd:#e7e5e4;--acc:#2563eb;--heat:37,99,235}}
@media (prefers-color-scheme:dark){{:root:not([data-theme="light"]){{--bg:#1c1917;--fg:#f5f5f4;--mut:#a8a29e;--card:#292524;--bd:#44403c;--acc:#60a5fa;--heat:96,165,250}}}}
:root[data-theme="dark"]{{--bg:#1c1917;--fg:#f5f5f4;--mut:#a8a29e;--card:#292524;--bd:#44403c;--acc:#60a5fa;--heat:96,165,250}}
body{{background:var(--bg);color:var(--fg);font:14px/1.45 system-ui,sans-serif;margin:0 auto;padding:16px;max-width:1250px}}h1{{font-size:20px;margin:4px 0}}h2{{font-size:16px;margin:26px 0 8px}}h3{{font-size:13px;color:var(--mut);margin:0 0 6px}}
.mut{{color:var(--mut)}}a{{color:var(--acc)}}.tiles{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin:12px 0}}.tile{{background:var(--card);border:1px solid var(--bd);border-radius:8px;padding:10px;color:var(--mut);font-size:12px}}.tile b{{display:block;font-size:26px;color:var(--fg)}}
.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:10px}}.card{{background:var(--card);border:1px solid var(--bd);border-radius:8px;padding:10px 12px;overflow-x:auto}}
.mx{{border-collapse:collapse;font-size:12px;width:100%}}.mx th,.mx td{{border:1px solid var(--bd);padding:3px 6px;text-align:right}}.mx td.l{{text-align:left}}.mx td.c{{background:rgba(var(--heat),calc(var(--a)*.55))}}
details{{background:var(--card);border:1px solid var(--bd);border-radius:8px;margin:8px 0;padding:6px 10px}}summary{{cursor:pointer}}
.list{{width:100%;border-collapse:collapse;font-size:12px;margin-top:6px}}.list th,.list td{{border-bottom:1px solid var(--bd);padding:3px 6px;text-align:left;vertical-align:top}}.list th{{color:var(--mut)}}
</style></head><body><h1>Review queue — open PRs waiting on us</h1><div class="mut">as of 2026-10-21 · {len(D)} non-draft open PRs · core = @ArthurZucker, @Cyrilvallez, @LysandreJik, @vasqu</div>
{tiles}<div class="grid">{matrix(wait, lambda r: r['bucket'], WB, 'Waiting for review: why × how long')}{matrix(appr, lambda r: r['primary'], sorted({r['primary'] for r in appr}), 'Approved, not merged: blocker × time since approval')}</div>
<h2>Approved but not merged ({len(appr)})</h2>{asec}<h2>Waiting for review ({len(wait)})</h2>{wsec}</body></html>"""
open('review_queue.html', 'w').write(page)
print('wait', Counter(r['bucket'] for r in wait), '| approved', len(appr), Counter(r['primary'] for r in appr))
print('ours >=90d:', sum(r['d'] >= 90 for r in ours), 'of', len(ours))
|