Module: tools Branch: master Commit: e71c99a61b6b7e9cb20c2c88ba43fa212b039cbb URL: https://gitlab.winehq.org/winehq/tools/-/commit/e71c99a61b6b7e9cb20c2c88ba43...
Author: Jeremy White jwhite@codeweavers.com Date: Wed Jul 13 16:52:11 2022 -0500
Assign reviewers to merge requests with none.
The algorithm is intended to function as follows:
1. Gather all entries where the F: pattern matches at least one file changed in the MR. The wildcard pattern (THE REST) is skipped.
2. Build the union of the M: fields of all matched entries. If empty, build the union of the P: fields instead. Assign all members of the union as reviewers. If empty, do nothing.
We ignore people that are not found in MAINTAINERS.
---
gitlab/gitlab-to-mail/assign.py | 130 ++++++++++++++++++++++++++++++++++ gitlab/gitlab-to-mail/gitlabtomail.py | 18 +++-- 2 files changed, 143 insertions(+), 5 deletions(-)
diff --git a/gitlab/gitlab-to-mail/assign.py b/gitlab/gitlab-to-mail/assign.py new file mode 100755 index 00000000..5a8b914d --- /dev/null +++ b/gitlab/gitlab-to-mail/assign.py @@ -0,0 +1,130 @@ +#!/usr/bin/env -S python3 -B + +""" +Determine who should review a given MR and assign them as reviewers +""" + +import sys +import copy +import re +from urllib.parse import urljoin +import fnmatch +import requests + +from util import fetch_all, Settings + +settings = Settings(sys.argv[1]) + +def empty_record(): + m = { 'name': None, + 'globs': [], + 'maintainers': [], + 'people': [], + } + return copy.deepcopy(m) + +def fetch_users(): + url = urljoin(settings.GITLAB_URL, f"api/v4/projects/{settings.GITLAB_PROJECT_ID}/users") + return fetch_all(url, settings) + +def append_user(user_map, line, out, verbose=False): + match = re.match("(.+)<", line) + if match: + name = match.group(1).strip() + else: + print("Malformed person in MAINTAINERS: {}".format(line), file=sys.stderr) + return + + if name in user_map: + out.append(user_map[name]) + elif verbose: + print("Cannot find GitLab account for [{}]".format(name), file=sys.stderr) + +def get_maintainers_map(verbose=False): + url = urljoin(settings.GITLAB_URL, f"{settings.GITLAB_PROJECT_NAME}/-/raw/master/MAINTAINERS") + r = requests.get(url, headers={"PRIVATE-TOKEN": settings.GITLAB_TOKEN}) + r = requests.get(url) + r.raise_for_status() + + users = fetch_users() + user_map = {} + for u in users: + user_map[u['name']] = u['id'] + + maintainers = [] + + m = empty_record() + for utf_line in r.iter_lines(): + line = utf_line.decode(r.encoding) + if not line or len(line) < 3: + continue + if line.find("F:\t") == 0: + # Skip THE REST patterns for now + if line[3:4] == '*': + continue + glob = line[3:] + # The Wine patterns are a bit unusual. They are file globs, + # but with an implicit trailing * if the target is a directory. + if glob[-1] == '/': + glob += '*' + m['globs'].append(glob) + elif line.find("M:\t") == 0: + append_user(user_map, line[3:], m['maintainers'], verbose) + elif line.find("P:\t") == 0: + append_user(user_map, line[3:], m['people'], verbose) + elif line.find("W:\t") == 0: + pass + elif len(m['globs']) == 0: + m['name'] = line + else: + maintainers.append(m) + m = empty_record() + m['name'] = line + + return maintainers + +def post_reviewers(mr_iid, reviewers): + url = urljoin(settings.GITLAB_URL, f"api/v4/projects/{settings.GITLAB_PROJECT_ID}/merge_requests/{mr_iid}/") + r = requests.put(url, headers={"PRIVATE-TOKEN": settings.GITLAB_TOKEN}, json={'reviewer_ids': reviewers}) + r.raise_for_status() + +def get_assignees(maintainers_map, files): + maintainers = [] + people = [] + for m in maintainers_map: + for glob in m['globs']: + for f in files: + if fnmatch.fnmatch(f, glob): + maintainers = maintainers + m['maintainers'] + people = people + m['people'] + if len(maintainers) > 0: + return maintainers + return people + +def assign_reviewers(mr_iid, version, maintainers_map, update_db): + paths = [] + if 'diffs' not in version: + return + for d in version['diffs']: + if 'new_path' in d: + paths.append(d['new_path']) + a = get_assignees(maintainers_map, paths) + if len(a) > 0: + if update_db: + # set() prunes dupes, list() makes it json transmittable again + post_reviewers(mr_iid, list(set(a))) + else: + print("Debug: would set reviewers for {} to ids {}".format(mr_iid, a)) + +def main(argv): + """ Debug code; pass in a config file and the names of files you want to test """ + maintainers_map = get_maintainers_map(True) + a = get_assignees(maintainers_map, argv[2:]) + users = fetch_users() + for id in a: + for u in users: + if id == u['id']: + print("{}: {}".format(id, u['name'])) + +if __name__ == "__main__": + main(sys.argv) diff --git a/gitlab/gitlab-to-mail/gitlabtomail.py b/gitlab/gitlab-to-mail/gitlabtomail.py index 03824504..67909567 100755 --- a/gitlab/gitlab-to-mail/gitlabtomail.py +++ b/gitlab/gitlab-to-mail/gitlabtomail.py @@ -13,6 +13,7 @@ import mailbox import smtplib import email from util import fetch_all, Settings +from assign import get_maintainers_map, assign_reviewers
settings = Settings(sys.argv[1])
@@ -570,7 +571,7 @@ def create_cover(mr_id, mr_iid, mr_version, versions, nr_patches, mr): return mail
-def process_mr(mr, update_db): +def process_mr(mr, update_db, maintainers_map): iid = mr['iid'] log(f"MR{iid} updated - processing")
@@ -623,6 +624,11 @@ def process_mr(mr, update_db): log(f"MR{iid}v{version} - skipping, has no changes") return
+ # Assign reviewers if there are none currently + if len(mr['reviewers']) == 0: + full_version = fetch_mr_version(iid, versions[0]['id']) + assign_reviewers(iid, full_version, maintainers_map, update_db) + fixup_date(cover, date) create_headers_from_mr(cover, mr) send_email(cover) @@ -647,12 +653,12 @@ def process_mr(mr, update_db): db.set_last_mr_updated_at(updated_at)
-def handle_debug_requests(): +def handle_debug_requests(maintainers_map): for arg in sys.argv[2:]: if arg.find("mr=") == 0: print(f"Processing MR iid {arg[3:]}") mr = fetch_specific_mr(int(arg[3:])) - process_mr(mr, False) + process_mr(mr, False, maintainers_map) elif arg.find("event=") == 0: print(f"Processing event id {arg[6:]}") # I did not immediately see a way to get a specific event. @@ -667,8 +673,10 @@ def handle_debug_requests():
def main():
+ maintainers_map = get_maintainers_map() + if len(sys.argv) > 2: - handle_debug_requests() + handle_debug_requests(maintainers_map) return
# Process any new merge requests @@ -679,7 +687,7 @@ def main(): db.set_last_mr_updated_at(last_mr_updated_at)
for mr in fetch_recently_updated_mrs(last_mr_updated_at): - process_mr(mr, not settings.READ_ONLY) + process_mr(mr, not settings.READ_ONLY, maintainers_map)
date = db.get_last_event_date() if not date: