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