Jeremy White : Assign reviewers to merge requests with none.
Alexandre Julliard
julliard at winehq.org
Fri Jul 15 05:07:51 CDT 2022
Module: tools
Branch: master
Commit: e71c99a61b6b7e9cb20c2c88ba43fa212b039cbb
URL: https://gitlab.winehq.org/winehq/tools/-/commit/e71c99a61b6b7e9cb20c2c88ba43fa212b039cbb
Author: Jeremy White <jwhite at 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:
More information about the wine-cvs
mailing list