scripts: introduce script to set labels, assignees and reviewers

Use MAINTAINERS.yml file to set lables, assignees and reviewers for
specific PRs or all unassigned PRs since a given date.

Signed-off-by: Anas Nashif <anas.nashif@intel.com>
Signed-off-by: Stephanos Ioannidis <root@stephanos.io>
diff --git a/scripts/set_assignees.py b/scripts/set_assignees.py
new file mode 100755
index 0000000..0e8c65e
--- /dev/null
+++ b/scripts/set_assignees.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2022 Intel Corp.
+# SPDX-License-Identifier: Apache-2.0
+
+import argparse
+import sys
+import os
+import time
+import datetime
+from github import Github, GithubException
+from collections import defaultdict
+
+TOP_DIR = os.path.join(os.path.dirname(__file__))
+sys.path.insert(0, os.path.join(TOP_DIR, "scripts"))
+from get_maintainer import Maintainers
+
+def log(s):
+    if args.verbose > 0:
+        print(s, file=sys.stdout)
+
+def parse_args():
+    global args
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+
+    parser.add_argument("-M", "--maintainer-file", required=False, default="MAINTAINERS.yml",
+                        help="Maintainer file to be used.")
+    parser.add_argument("-P", "--pull_request", required=False, default=None, type=int,
+                        help="Operate on one pull-request only.")
+    parser.add_argument("-s", "--since", required=False,
+                        help="Process pull-requests since date.")
+
+    parser.add_argument("-y", "--dry-run", action="store_true", default=False,
+                        help="Dry run only.")
+
+    parser.add_argument("-o", "--org", default="zephyrproject-rtos",
+                        help="Github organisation")
+
+    parser.add_argument("-r", "--repo", default="zephyr",
+                        help="Github repository")
+
+    parser.add_argument("-v", "--verbose", action="count", default=0,
+                        help="Verbose Output")
+
+    args = parser.parse_args()
+
+def process_pr(gh, maintainer_file, number):
+
+    gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
+    pr = gh_repo.get_pull(number)
+
+    log(f"working on https://github.com/{args.org}/{args.repo}/pull/{pr.number} : {pr.title}")
+
+    labels = set()
+    collab = set()
+    area_counter = defaultdict(int)
+    maint = defaultdict(int)
+
+    num_files = 0
+    all_areas = set()
+    fn = list(pr.get_files())
+    if len(fn) > 500:
+        log(f"Too many files changed ({len(fn)}), skipping....")
+        return
+    for f in pr.get_files():
+        num_files += 1
+        log(f"file: {f.filename}")
+        areas = maintainer_file.path2areas(f.filename)
+
+        if areas:
+            all_areas.update(areas)
+            for a in areas:
+                area_counter[a.name] += 1
+                labels.update(a.labels)
+                collab.update(a.collaborators)
+                collab.update(a.maintainers)
+                for p in a.maintainers:
+                    maint[p] += 1
+
+    ac = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
+    log(f"Area matches: {ac}")
+    log(f"labels: {labels}")
+    log(f"collab: {collab}")
+    if len(labels) > 10:
+        log(f"Too many labels to be applied")
+        return
+
+    sm = dict(sorted(maint.items(), key=lambda item: item[1], reverse=True))
+
+    log(f"Submitted by: {pr.user.login}")
+    log(f"candidate maintainers: {sm}")
+
+    prop = 0
+    if sm:
+        maintainer = list(sm.keys())[0]
+
+        if len(ac) > 1 and list(ac.values())[0] == list(ac.values())[1]:
+            log("++ Platform/Drivers takes precedence over subsystem...")
+            for aa in ac:
+                if 'Documentation' in aa:
+                    log("++ With multiple areas of same weight including docs, take something else other than Documentation as the maintainer")
+                    for a in all_areas:
+                        if a.name == aa and a.maintainers[0] == maintainer:
+                            maintainer = list(sm.keys())[1]
+                elif 'Platform' in aa:
+                    log(f"Set maintainer of area {aa}")
+                    for a in all_areas:
+                        if a.name == aa:
+                            if a.maintainers:
+                                maintainer = a.maintainers[0]
+                                break
+
+
+        # if the submitter is the same as the maintainer, check if we have
+        # multiple maintainers
+        if pr.user.login == maintainer:
+            log("Submitter is same as Assignee, trying to find another assignee...")
+            aff = list(ac.keys())[0]
+            for a in all_areas:
+                if a.name == aff:
+                    if len(a.maintainers) > 1:
+                        maintainer = a.maintainers[1]
+                    else:
+                        log(f"This area has only one maintainer, keeping assignee as {maintainer}")
+
+        prop = (maint[maintainer] / num_files) * 100
+        if prop < 20:
+            maintainer = "None"
+    else:
+        maintainer = "None"
+    log(f"Picked maintainer: {maintainer} ({prop:.2f}% ownership)")
+    log("+++++++++++++++++++++++++")
+
+    # Set labels
+    if labels and len(labels) < 10:
+        for l in labels:
+            log(f"adding label {l}...")
+            if not args.dry_run:
+                pr.add_to_labels(l)
+
+    if collab:
+        reviewers = []
+        existing_reviewers = set()
+
+        revs = pr.get_reviews()
+        for review in revs:
+            existing_reviewers.add(review.user)
+
+        rl = pr.get_review_requests()
+        page = 0
+        for r in rl:
+            existing_reviewers |= set(r.get_page(page))
+            page += 1
+
+        for c in collab:
+            u = gh.get_user(c)
+            if pr.user != u and gh_repo.has_in_collaborators(u):
+                if u not in existing_reviewers:
+                    reviewers.append(c)
+
+        if reviewers:
+            try:
+                log(f"adding reviewers {reviewers}...")
+                if not args.dry_run:
+                    pr.create_review_request(reviewers=reviewers)
+            except GithubException:
+                log("cant add reviewer")
+
+    ms = []
+    # assignees
+    if maintainer != 'None':
+        try:
+            u = gh.get_user(maintainer)
+            ms.append(u)
+        except GithubException:
+            log(f"Error: Unknown user")
+
+        for mm in ms:
+            log(f"Adding assignee {mm}...")
+            if not args.dry_run:
+                pr.add_to_assignees(mm)
+
+    time.sleep(1)
+
+def main():
+    parse_args()
+
+    token = os.environ.get('GITHUB_TOKEN', None)
+    if not token:
+        sys.exit('Github token not set in environment, please set the '
+                 'GITHUB_TOKEN environment variable and retry.')
+
+    gh = Github(token)
+    maintainer_file = Maintainers(args.maintainer_file)
+
+    if args.pull_request:
+        process_pr(gh, maintainer_file, args.pull_request)
+    else:
+        if args.since:
+            since = args.since
+        else:
+            today = datetime.date.today()
+            since = today - datetime.timedelta(days=1)
+
+        common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
+        pulls = gh.search_issues(query=f'{common_prs}')
+
+        for issue in pulls:
+            process_pr(gh, maintainer_file, issue.number)
+
+
+if __name__ == "__main__":
+    main()