tof-voters: Record name and email in analysis

With this we can make the data a little easier to consume.

Signed-off-by: Andrew Jeffery <andrew@aj.id.au>
Change-Id: Id1cc858dc002adfdf35e2a7bb790dc4872d9c570
diff --git a/tof-voters/README.md b/tof-voters/README.md
index 1d733cc..dd61232 100644
--- a/tof-voters/README.md
+++ b/tof-voters/README.md
@@ -15,8 +15,7 @@
 ./voters analyze-commits --before "2022-01-01" --after "2021-06-30"
 ./voters analyze-reviews --before "2022-01-01" --after "2021-06-30"
 ./voters report
-cat data/report.json | \
-    jq "with_entries(select(.value.qualified) | .value = .value.points)"
+jq '[ .[] | select(.qualified) | {(.name): .points} ] | add | to_entries | sort_by(.value) | reverse | from_entries' data/report.json
 ```
 
 The above will yield a JSON dictionary of "users:points" where 'qualified' is
@@ -24,8 +23,8 @@
 
 ```json
 {
-    "user1": 16,
-    "user2": 19,
+    "User Name 2": 19,
+    "User Name 1": 16,
     ...
 }
 ```
diff --git a/tof-voters/libvoters/__init__.py b/tof-voters/libvoters/__init__.py
index a93a4bf..9a2c4a6 100644
--- a/tof-voters/libvoters/__init__.py
+++ b/tof-voters/libvoters/__init__.py
@@ -1 +1,18 @@
 #!/usr/bin/python3
+
+from typing import TypedDict
+
+UserChanges = TypedDict(
+    "User", {"name": str, "email": str, "changes": list[int]}
+)
+
+
+def changes_factory():
+    return {"name": None, "email": None, "changes": list()}
+
+
+UserComments = TypedDict("User", {"name": str, "email": str, "comments": int})
+
+
+def comments_factory():
+    return {"name": None, "email": None, "comments": 0}
diff --git a/tof-voters/libvoters/subcmd/analyze-commits.py b/tof-voters/libvoters/subcmd/analyze-commits.py
index ea92ed1..7957319 100644
--- a/tof-voters/libvoters/subcmd/analyze-commits.py
+++ b/tof-voters/libvoters/subcmd/analyze-commits.py
@@ -8,6 +8,7 @@
 from typing import Dict
 
 import libvoters.acceptable as acceptable
+from libvoters import UserChanges, changes_factory
 from libvoters.time import TimeOfDay, timestamp
 
 
@@ -36,7 +37,7 @@
         before = timestamp(args.before, TimeOfDay.AM)
         after = timestamp(args.after, TimeOfDay.PM)
 
-        changes_per_user: Dict[str, list[int]] = defaultdict(list)
+        changes_per_user: Dict[str, UserChanges] = defaultdict(changes_factory)
 
         for f in sorted(os.listdir(args.dir)):
             path = os.path.join(args.dir, f)
@@ -69,7 +70,7 @@
 
             project = data["project"]
             id_number = data["number"]
-            user = data["owner"]["username"]
+            username = data["owner"]["username"]
 
             if not acceptable.project(project):
                 print("Rejected project:", project, id_number)
@@ -93,11 +94,14 @@
                 print("Rejected for limited changes:", project, id_number)
                 continue
 
-            print(project, id_number, user)
+            print(project, id_number, username)
             for f in touched_files:
                 print(f"    {f}")
 
-            changes_per_user[user].append(id_number)
+            user = changes_per_user[username]
+            user["name"] = data["owner"]["name"]
+            user["email"] = data["owner"]["email"]
+            user["changes"].append(id_number)
 
         with open(os.path.join(args.dir, "commits.json"), "w") as outfile:
             outfile.write(json.dumps(changes_per_user, indent=4))
diff --git a/tof-voters/libvoters/subcmd/analyze-reviews.py b/tof-voters/libvoters/subcmd/analyze-reviews.py
index d5ccade..244e9bc 100644
--- a/tof-voters/libvoters/subcmd/analyze-reviews.py
+++ b/tof-voters/libvoters/subcmd/analyze-reviews.py
@@ -8,6 +8,7 @@
 from typing import Dict
 
 import libvoters.acceptable as acceptable
+from libvoters import UserChanges, UserComments, changes_factory, comments_factory
 from libvoters.time import TimeOfDay, timestamp
 
 
@@ -36,7 +37,7 @@
         before = timestamp(args.before, TimeOfDay.AM)
         after = timestamp(args.after, TimeOfDay.PM)
 
-        changes_per_user: Dict[str, list[int]] = defaultdict(list)
+        changes_per_user: Dict[str, UserChanges] = defaultdict(changes_factory)
 
         for f in sorted(os.listdir(args.dir)):
             path = os.path.join(args.dir, f)
@@ -56,7 +57,9 @@
             if not acceptable.project(project):
                 print("Rejected project:", project, id_number)
 
-            comments_per_user: Dict[str, int] = defaultdict(int)
+            comments_per_user: Dict[str, UserComments] = defaultdict(
+                comments_factory
+            )
 
             for patch_set in data["patchSets"]:
                 created_on = data["createdOn"]
@@ -75,14 +78,40 @@
                     if not acceptable.file(project, comment["file"]):
                         continue
 
-                    comments_per_user[reviewer] += 1
+                    user = comments_per_user[reviewer]
+                    user["name"] = comment["reviewer"]["name"]
+                    # We actually have a case where a reviewer does not have an email recorded[1]:
+                    #
+                    # [1]: https://gerrit.openbmc.org/c/openbmc/phosphor-pid-control/+/60303/comment/ceff60b9_9d2debe0/
+                    #
+                    # {"file": "conf.hpp",
+                    #  "line": 39,
+                    #  "reviewer": {"name": "Akshat Jain", "username": "AkshatZen"},
+                    #  "message": "If we design SensorInput as base class and have derived ..."}
+                    # Traceback (most recent call last):
+                    #   File "/mnt/host/andrew/home/andrew/src/openbmc/openbmc-tools/tof-voters/./voters", line 7, in <module>
+                    #     sys.exit(main())
+                    #              ^^^^^^
+                    #   File "/mnt/host/andrew/home/andrew/src/openbmc/openbmc-tools/tof-voters/libvoters/entry_point.py", line 33, in main
+                    #     return int(args.cmd.run(args))
+                    #                ^^^^^^^^^^^^^^^^^^
+                    #   File "/mnt/host/andrew/home/andrew/src/openbmc/openbmc-tools/tof-voters/libvoters/subcmd/analyze-reviews.py", line 82, in run
+                    #     user["email"] = comment["reviewer"]["email"]
+                    #                     ~~~~~~~~~~~~~~~~~~~^^^^^^^^^
+                    # KeyError: 'email'
+                    if "email" in comment["reviewer"]:
+                        user["email"] = comment["reviewer"]["email"]
+                    user["comments"] += 1
 
             print(project, id_number)
-            for user, count in comments_per_user.items():
-                if count < 3:
+            for username, review in comments_per_user.items():
+                if review["comments"] < 3:
                     continue
-                print("    ", user, count)
-                changes_per_user[user].append(id_number)
+                print("    ", user, review["comments"])
+                user = changes_per_user[username]
+                user["name"] = review["name"]
+                user["email"] = review["email"]
+                user["changes"].append(id_number)
 
         with open(os.path.join(args.dir, "reviews.json"), "w") as outfile:
             outfile.write(json.dumps(changes_per_user, indent=4))
diff --git a/tof-voters/libvoters/subcmd/report.py b/tof-voters/libvoters/subcmd/report.py
index 87aa713..87c87ca 100644
--- a/tof-voters/libvoters/subcmd/report.py
+++ b/tof-voters/libvoters/subcmd/report.py
@@ -31,9 +31,11 @@
         with open(reviews_fp, "r") as reviews_file:
             reviews = json.load(reviews_file)
 
-        for user in sorted(set(commits.keys()).union(reviews.keys())):
-            user_commits = len(commits.get(user, []))
-            user_reviews = len(reviews.get(user, []))
+        contributions = commits | reviews
+
+        for user in sorted(contributions.keys()):
+            user_commits = len(commits.get(user, {}).get("changes", []))
+            user_reviews = len(reviews.get(user, {}).get("changes", []))
 
             points = user_commits * 3 + user_reviews
             print(user, points, user_commits, user_reviews)
@@ -41,6 +43,8 @@
             qualified = points >= 15
 
             results[user] = {
+                "name": contributions[user]["name"],
+                "email": contributions[user]["email"],
                 "qualified": qualified,
                 "points": points,
                 "commits": user_commits,
diff --git a/tof-voters/voters b/tof-voters/voters
index 75c105e..4129d84 100755
--- a/tof-voters/voters
+++ b/tof-voters/voters
@@ -1,7 +1,8 @@
 #!/bin/env -S python3 -B
 
-from libvoters.entry_point import main
 import sys
 
+from libvoters.entry_point import main
+
 if __name__ == "__main__":
     sys.exit(main())