import json import urllib.error import urllib.request from base64 import b64encode token = b":" def read_json_url(url,skip_lines=0): with urllib.request.urlopen(url) as f: for _ in range(skip_lines): f.readline() return json.load(f) def authenticated_request(url): req = urllib.request.Request("https://gerrit.wikimedia.org/r/a"+url) req.add_header("Authorization",b"Basic "+b64encode(token)) return read_json_url(req,1) def get_projects(): for name, data in authenticated_request("/projects/?r=mediawiki/extensions/.*").items(): state = data["state"] if state == "ACTIVE": yield name, data["id"] def get_group_members(group): data = authenticated_request(f"/groups/{group}/detail/") members = set() for member in data["members"]: members.add(member["_account_id"]) for included in data["includes"]: if included["id"] != group: # Suppress infinite recursion if a group is a member of itself # happens with WMF Campaigns team members.update(get_group_members(included["id"])) return members def get_project_access(project): access = authenticated_request(f"/projects/{project}/access/") inherits_from = access["inherits_from"]["id"] allowed = set() if inherits_from != "mediawiki%2Fextensions": allowed.update(get_project_access(inherits_from)) try: groups = access["local"]["refs/*"]["permissions"]["owner"]["rules"] except KeyError: # Suppress KeyError if a project has no groups at all # (This happens with DynamicSidebar) #print(f"*** {project} has no owners") return set() for groupid, perms in groups.items(): if perms != {"action":"ALLOW","force":False}: #print("f**** {project} has nonstandard permissions") continue try: allowed.update(get_group_members(groupid)) except urllib.error.HTTPError as error: # Suppress 404 error in case a group is misconfigured # (This happens with AddPersonalURLs, DynamicPageListEngine, some others) if error.code != 404: raise error.close() #print(f"*** {project} has misconfigured groups") return allowed def check_dates(date="2022-01-21",start=""): for repo, id_ in get_projects(): if repo < start: continue members = get_project_access(id_) changes = authenticated_request(f"/changes/?q=status:merged+" f"after:{date}+project:{repo}" f"&o=DETAILED_ACCOUNTS") for change in changes: if change["owner"]["_account_id"] in (137, 5266): # Ignore l10n-bot/Libraryupgrader continue elif change["submit_records"] == []: # Open-pushed change indicates maintenance #print(f"{repo}: Patch {change['_number']} pushed without approval") break else: for label in change["submit_records"][0]["labels"]: if label["label"] == "Code-Review": if label["applied_by"]["_account_id"] in members: #print(f"{repo}: Patch {change['_number']} was approved" # "by a member of the repository") break else: continue break else: print(repo) check_dates()