From 9741f162420e95628876b1b7f52d7ccd6a19876a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 Dec 2023 11:13:04 +0100
Subject: [PATCH 1/3] Bump actions/setup-python from 4 to 5 (#207)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)
---
updated-dependencies:
- dependency-name: actions/setup-python
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci-format.yml | 2 +-
.github/workflows/reusable-sphinx-check-single-version.yml | 2 +-
.github/workflows/sphinx-check-links.yml | 2 +-
.github/workflows/sphinx-check-warnings-pr.yml | 2 +-
.github/workflows/sphinx-check-warnings.yml | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml
index 1fdcef1d03e..0820597d9bc 100644
--- a/.github/workflows/ci-format.yml
+++ b/.github/workflows/ci-format.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-python@v4.7.0
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
- uses: pre-commit/action@v3.0.0
diff --git a/.github/workflows/reusable-sphinx-check-single-version.yml b/.github/workflows/reusable-sphinx-check-single-version.yml
index 97880acbca7..3903f4f4f42 100644
--- a/.github/workflows/reusable-sphinx-check-single-version.yml
+++ b/.github/workflows/reusable-sphinx-check-single-version.yml
@@ -22,7 +22,7 @@ jobs:
repository: ros-controls/control.ros.org
ref: master
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
diff --git a/.github/workflows/sphinx-check-links.yml b/.github/workflows/sphinx-check-links.yml
index cbcd7d7b566..eb1d7a1e02e 100644
--- a/.github/workflows/sphinx-check-links.yml
+++ b/.github/workflows/sphinx-check-links.yml
@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
diff --git a/.github/workflows/sphinx-check-warnings-pr.yml b/.github/workflows/sphinx-check-warnings-pr.yml
index c745fc6e414..551c7ec41e6 100644
--- a/.github/workflows/sphinx-check-warnings-pr.yml
+++ b/.github/workflows/sphinx-check-warnings-pr.yml
@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
diff --git a/.github/workflows/sphinx-check-warnings.yml b/.github/workflows/sphinx-check-warnings.yml
index 3d9904efb23..b101970293d 100644
--- a/.github/workflows/sphinx-check-warnings.yml
+++ b/.github/workflows/sphinx-check-warnings.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
From 01ec224e976543b17fb696fb2c5fd6ae083feb89 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 Dec 2023 11:13:49 +0100
Subject: [PATCH 2/3] Bump actions/checkout from 3 to 4 (#206)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci-format.yml | 2 +-
.github/workflows/reusable-sphinx-check-single-version.yml | 2 +-
.github/workflows/sphinx-check-links.yml | 2 +-
.github/workflows/sphinx-check-warnings-pr.yml | 2 +-
.github/workflows/sphinx-check-warnings.yml | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml
index 0820597d9bc..a5ccb165c54 100644
--- a/.github/workflows/ci-format.yml
+++ b/.github/workflows/ci-format.yml
@@ -12,7 +12,7 @@ jobs:
name: Format
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
diff --git a/.github/workflows/reusable-sphinx-check-single-version.yml b/.github/workflows/reusable-sphinx-check-single-version.yml
index 3903f4f4f42..314e5ec0e7f 100644
--- a/.github/workflows/reusable-sphinx-check-single-version.yml
+++ b/.github/workflows/reusable-sphinx-check-single-version.yml
@@ -17,7 +17,7 @@ jobs:
build-single-version:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
repository: ros-controls/control.ros.org
ref: master
diff --git a/.github/workflows/sphinx-check-links.yml b/.github/workflows/sphinx-check-links.yml
index eb1d7a1e02e..9fe667daeec 100644
--- a/.github/workflows/sphinx-check-links.yml
+++ b/.github/workflows/sphinx-check-links.yml
@@ -13,7 +13,7 @@ jobs:
check-links:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
diff --git a/.github/workflows/sphinx-check-warnings-pr.yml b/.github/workflows/sphinx-check-warnings-pr.yml
index 551c7ec41e6..fda7ed4e9fb 100644
--- a/.github/workflows/sphinx-check-warnings-pr.yml
+++ b/.github/workflows/sphinx-check-warnings-pr.yml
@@ -7,7 +7,7 @@ jobs:
build-singleversion-halt-on-warnings:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
diff --git a/.github/workflows/sphinx-check-warnings.yml b/.github/workflows/sphinx-check-warnings.yml
index b101970293d..8a41167b84e 100644
--- a/.github/workflows/sphinx-check-warnings.yml
+++ b/.github/workflows/sphinx-check-warnings.yml
@@ -12,7 +12,7 @@ jobs:
build-halt-on-warnings:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
From 475365b92c5101b64ba18059e91b122af78ad18e Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2023 10:34:06 -0600
Subject: [PATCH 3/3] Add reviewer_stats (backport #204) (#211)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [Contributions] Add reviewers statistics (#204)
(cherry picked from commit 5e1f1d647219a12c26528ea9af4a92513f6b0940)
# Conflicts:
# .github/workflows/sphinx-check-warnings-humble.yml
# .github/workflows/sphinx-check-warnings-rolling.yml
# .github/workflows/sphinx-make-page.yml
* Delete unneeded jobs
* Delete unneeded jobs
* Update make_help_scripts/deploy_defines
Co-authored-by: Sai Kishor Kothakota
---------
Co-authored-by: Christoph Fröhlich
Co-authored-by: Christoph Froehlich
Co-authored-by: Sai Kishor Kothakota
---
.github/workflows/ci-format.yml | 9 +
.github/workflows/sphinx-check-links.yml | 7 +
...ngs.yml => sphinx-check-warnings-iron.yml} | 9 +-
.../workflows/sphinx-check-warnings-pr.yml | 7 +
README.md | 8 +
_static/reviewer_stats.css | 41 ++
conf.py | 8 +-
doc/acknowledgements/acknowledgements.rst | 43 ++
make_help_scripts/add_review_stats | 9 +
make_help_scripts/add_sub_repos | 1 +
make_help_scripts/add_tmp_commits | 1 +
make_help_scripts/create_reviewer_stats.py | 538 ++++++++++++++++++
make_help_scripts/deploy_defines | 22 +
requirements.txt | 1 +
14 files changed, 702 insertions(+), 2 deletions(-)
rename .github/workflows/{sphinx-check-warnings.yml => sphinx-check-warnings-iron.yml} (82%)
create mode 100644 _static/reviewer_stats.css
create mode 100755 make_help_scripts/add_review_stats
create mode 100644 make_help_scripts/create_reviewer_stats.py
diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml
index a5ccb165c54..af0c354d63d 100644
--- a/.github/workflows/ci-format.yml
+++ b/.github/workflows/ci-format.yml
@@ -16,6 +16,15 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.10'
+ - name: Restore stats
+ uses: actions/cache/restore@v3
+ with:
+ key: reviewer-stats-${{ github.run_id }}
+ restore-keys: |
+ reviewer-stats
+ path: ~/reviews
+ - name: Copy stats
+ run: ./make_help_scripts/add_review_stats
- uses: pre-commit/action@v3.0.0
with:
extra_args: --all-files --hook-stage manual
diff --git a/.github/workflows/sphinx-check-links.yml b/.github/workflows/sphinx-check-links.yml
index 9fe667daeec..2c5ea4d3547 100644
--- a/.github/workflows/sphinx-check-links.yml
+++ b/.github/workflows/sphinx-check-links.yml
@@ -34,6 +34,13 @@ jobs:
shell: bash
- name: Install doxygen and graphviz
run: sudo apt-get install -y doxygen graphviz
+ - name: Restore stats
+ uses: actions/cache/restore@v3
+ with:
+ key: reviewer-stats-${{ github.run_id }}
+ restore-keys: |
+ reviewer-stats
+ path: ~/reviews
- name: Build page with API and run linkchecker
run: |
git config --local user.email "action@github.com"
diff --git a/.github/workflows/sphinx-check-warnings.yml b/.github/workflows/sphinx-check-warnings-iron.yml
similarity index 82%
rename from .github/workflows/sphinx-check-warnings.yml
rename to .github/workflows/sphinx-check-warnings-iron.yml
index 8a41167b84e..34dac1f07fe 100644
--- a/.github/workflows/sphinx-check-warnings.yml
+++ b/.github/workflows/sphinx-check-warnings-iron.yml
@@ -1,4 +1,4 @@
-name: "Check Page for Warnings"
+name: "Check Page for Warnings Iron"
on:
workflow_dispatch:
push:
@@ -31,6 +31,13 @@ jobs:
cd generate_parameter_library/generate_parameter_library_py/
python -m pip install .
shell: bash
+ - name: Restore stats
+ uses: actions/cache/restore@v3
+ with:
+ key: reviewer-stats-${{ github.run_id }}
+ restore-keys: |
+ reviewer-stats
+ path: ~/reviews
- name: Build single version considering warnings as errors
run: |
git config --local user.email "action@github.com"
diff --git a/.github/workflows/sphinx-check-warnings-pr.yml b/.github/workflows/sphinx-check-warnings-pr.yml
index fda7ed4e9fb..83aadd2baef 100644
--- a/.github/workflows/sphinx-check-warnings-pr.yml
+++ b/.github/workflows/sphinx-check-warnings-pr.yml
@@ -26,6 +26,13 @@ jobs:
cd generate_parameter_library/generate_parameter_library_py/
python -m pip install .
shell: bash
+ - name: Restore stats
+ uses: actions/cache/restore@v3
+ with:
+ key: reviewer-stats-${{ github.run_id }}
+ restore-keys: |
+ reviewer-stats
+ path: ~/reviews
- name: Build single version considering warnings as errors
run: |
git config --local user.email "action@github.com"
diff --git a/README.md b/README.md
index 805aa5ef005..437ab25628a 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,14 @@ There are `make` commands available which automate the process of building and i
* ```make multiversion``` - Builds multiversion version, changes are only visible after commit. **Make sure to commit everything before running!**
* For each command, a ```make -with-api``` exists, which in addition builds the `doxygen` api.
+# Fetch reviewer stats
+First, you need to fetch the reviewer stats from ros2_control org. To do so, you need to have a github token with the `repo` scope. Then run
+
+```bash
+export GITHUB_TOKEN=
+python3 ./make_help_scripts/create_reviewer_stats.py
+```
+which will create `~/reviews/reviewers_stats_with_graph.html`. Then you can build the documentation as usual, it will copy the file from this folder.
# Build Instructions:
1. If you are running inside a docker container, be sure to open a port so the website can be accessed.
diff --git a/_static/reviewer_stats.css b/_static/reviewer_stats.css
new file mode 100644
index 00000000000..14654506344
--- /dev/null
+++ b/_static/reviewer_stats.css
@@ -0,0 +1,41 @@
+/* reviewer_stats.css */
+table {
+ font-family: Arial, sans-serif;
+ border-collapse: collapse;
+ width: 100%;
+}
+
+th, td {
+ border: 1px solid #dddddd;
+ text-align: left;
+ padding: 8px;
+}
+
+tr:nth-child(even) {
+ background-color: #f2f2f2;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 20px;
+ margin: 0;
+ background-color: #ddd;
+ border-radius: 5px;
+ overflow: hidden;
+}
+
+.progress-value-reviews {
+ display: block;
+ height: 100%;
+ width: 0;
+ background-color: #2980b9;
+ border-radius: 5px;
+}
+
+.progress-value-ratio {
+ display: block;
+ height: 100%;
+ width: 0;
+ background-color: rgba(47, 64, 95, 0.5); /* Adjusted to 50% transparent */
+ border-radius: 5px;
+}
diff --git a/conf.py b/conf.py
index 9c84186841b..9563b6c15cf 100644
--- a/conf.py
+++ b/conf.py
@@ -124,7 +124,13 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-# html_static_path = ["_static"]
+html_static_path = ["_static"]
+
+# These paths are either relative to html_static_path
+# or fully qualified paths (eg. https://...)
+html_css_files = [
+ 'reviewer_stats.css',
+]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
diff --git a/doc/acknowledgements/acknowledgements.rst b/doc/acknowledgements/acknowledgements.rst
index 78fd814d7aa..89d9795f1fd 100644
--- a/doc/acknowledgements/acknowledgements.rst
+++ b/doc/acknowledgements/acknowledgements.rst
@@ -2,6 +2,49 @@
Acknowledgements
================
+
+Maintainers
+----------------
+The following people were maintaining the ``ros2_control`` framework, showing their all-time review activity:
+
+.. raw:: html
+ :file: maintainers_stats.html
+
+Activity during the past 12 months:
+
+.. raw:: html
+ :file: maintainers_stats_recent.html
+
+Reviewers' Stats
+----------------
+The following people have contributed to the development of this project by giving valuable reviews for pull requests, see :ref:`doc/contributing/contributing:contributing` for more information.
+
+.. raw:: html
+ :file: reviewers_stats.html
+
+Activity during the past 12 months:
+
+.. raw:: html
+ :file: reviewers_stats_recent.html
+
+Contributors
+----------------
+
+The following links lists people who have contributed to the development of this project by submitting pull requests to the respective repository, see :ref:`doc/contributing/contributing:contributing` for more information.
+
+* `ros2_control `_
+* `ros2_controllers `_
+* `ros2_control_demos `_
+* `control_toolbox `_
+* `gazebo_ros2_control `_
+* `gz_ros2_control `_
+* `realtime_tools `_
+* `kinematics_interface `_
+* `control_msgs `_
+
+
+Companies and Institutions
+--------------------------
The project has received major contributions from the following companies and institutions.
|palroboticslogo|
diff --git a/make_help_scripts/add_review_stats b/make_help_scripts/add_review_stats
new file mode 100755
index 00000000000..e4cb5af2673
--- /dev/null
+++ b/make_help_scripts/add_review_stats
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+# shellckeck source=deploy_defines
+# source deploy_defines regardless of startingpoint
+DIR="${BASH_SOURCE%/*}"
+if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
+. "$DIR/deploy_defines" || { echo "Could not source deploy_defines script. This is needed for correct execution. Exiting!"; exit; }
+
+add_reviewer_stats_file
diff --git a/make_help_scripts/add_sub_repos b/make_help_scripts/add_sub_repos
index c5a18cdb9a4..2095593e969 100755
--- a/make_help_scripts/add_sub_repos
+++ b/make_help_scripts/add_sub_repos
@@ -23,3 +23,4 @@ add_sub_repositories () {
}
add_sub_repositories
+add_reviewer_stats_file
diff --git a/make_help_scripts/add_tmp_commits b/make_help_scripts/add_tmp_commits
index 8907ba4f758..affc1fee5b3 100755
--- a/make_help_scripts/add_tmp_commits
+++ b/make_help_scripts/add_tmp_commits
@@ -46,6 +46,7 @@ add_sub_repositories_and_commit () {
rm -rf .git/
cd ../../
done
+ add_reviewer_stats_file
git add .
# we don't want to use-precommit to check if subrepos are correct
git commit -m "Add temporary changes for multi version" --no-verify 1> /dev/null
diff --git a/make_help_scripts/create_reviewer_stats.py b/make_help_scripts/create_reviewer_stats.py
new file mode 100644
index 00000000000..29c24a7c082
--- /dev/null
+++ b/make_help_scripts/create_reviewer_stats.py
@@ -0,0 +1,538 @@
+# Copyright (c) 2023 ros2_control maintainers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import requests
+import os
+from datetime import datetime, timedelta
+import time
+
+def get_api_response(url):
+ """
+ Sends a GET request to the specified URL with the necessary headers and returns the JSON response.
+
+ Args:
+ url (str): The URL to send the GET request to.
+
+ Returns:
+ tuple: A tuple containing the JSON response as a dictionary and the response object.
+ """
+ global_header = {
+ "Accept": "application/vnd.github.v3+json",
+ "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"
+ }
+ # TODO(anyone): add error handling
+ response = requests.get(url, headers=global_header)
+ if response.status_code != 200:
+ print(f"Error {response.status_code}: {response.json()['message']}")
+ return [], response
+ json = response.json()
+
+ return json, response
+
+def get_api_response_wait(url):
+ """
+ Waits for the API rate limit reset if the remaining requests are zero,
+ then returns the API response for the given URL.
+
+ Args:
+ url (str): The URL to send the API request to.
+
+ Returns:
+ tuple: A tuple containing the JSON response as a dictionary and the response object.
+ """
+ remaining, reset = get_api_limit()
+ if remaining == 0:
+ wait_time = datetime.fromtimestamp(reset) - datetime.utcnow()
+ print(f"Waiting {wait_time.total_seconds()} seconds for API rate limit reset")
+ time.sleep(wait_time.total_seconds() + 60) # add 60 seconds to be sure
+
+ return get_api_response(url)
+
+def get_api_limit():
+ """
+ Retrieves the remaining API rate limit and reset time from the GitHub API.
+
+ Returns:
+ A tuple containing the remaining API rate limit and the reset time.
+ """
+ url = "https://api.github.com/rate_limit"
+ json, response = get_api_response(url)
+
+ if json:
+ return json["rate"]["remaining"], json["rate"]["reset"]
+ else:
+ return 0, 0
+
+
+def get_all_pages(url):
+ """
+ Generator function, retrieves all pages of data from the given URL.
+
+ Args:
+ url (str): The URL to retrieve data from.
+
+ Yields:
+ dict: A JSON object representing a page of data.
+
+ Returns:
+ None
+ """
+ while url:
+ json, response = get_api_response_wait(url)
+
+ yield json
+
+ if 'next' in response.links:
+ url = response.links['next']['url']
+ else:
+ url = None
+
+
+def get_user_name(user):
+ """
+ Retrieves the name of a GitHub user.
+
+ Args:
+ user (str): The GitHub login name.
+
+ Returns:
+ str: The name of the GitHub user, or an empty string if the name is not available.
+ """
+ url = f"https://api.github.com/users/{user}"
+ json, response = get_api_response_wait(url)
+
+ if json:
+ return json["name"]
+ else:
+ return ""
+
+
+def get_reviewers_stats(owner, repos, branches, whitelist, earliest_date=""):
+ """
+ Retrieves statistics about reviewers' activity on pull requests.
+
+ Args:
+ owner (str): The owner of the repositories.
+ repos (list): The list of repositories.
+ branches (dict): The dictionary mapping repositories to their branches.
+ whitelist (list): The list of whitelisted reviewers.
+ earliest_date (str, optional): The earliest date to consider for reviews. Defaults to "".
+
+ Returns:
+ tuple: A tuple containing the following elements:
+ - reviewers (dict): A dictionary containing statistics for all reviewers not in whitelist.
+ - reviewers_whitelist (dict): A dictionary containing statistics for whitelisted reviewers.
+ - reviewers_filter (dict): A dictionary containing filtered statistics for all reviewers not in whitelist.
+ - reviewers_filter_whitelist (dict): A dictionary containing filtered statistics for whitelisted reviewers.
+ - ct_pull (int): The total number of pull requests processed.
+ """
+
+ reviewers = {}
+ reviewers_whitelist = {}
+ reviewers_filter = {}
+ reviewers_filter_whitelist = {}
+ ct_pull = 0
+
+ for repo in repos:
+ print(f"Getting reviewers' stats for {owner}/{repo} on branch {branches[repo]}")
+
+ url = f"https://api.github.com/repos/{owner}/{repo}/pulls?state=closed&base={branches[repo]}&per_page=100"
+
+ for pulls in get_all_pages(url):
+
+ for pull in pulls:
+ ct_pull += 1
+
+ # parse requested reviewers
+ reviewers_list = pull["requested_reviewers"]
+ for reviewer in reviewers_list:
+ reviewer_login = reviewer["login"]
+
+ if reviewer_login in whitelist:
+ current_dict = reviewers_whitelist
+ else:
+ current_dict = reviewers
+
+ if reviewer_login in current_dict:
+ current_dict[reviewer_login]["assigned_reviews"] += 1
+ else:
+ current_dict[reviewer_login] = {
+ "avatar_url": reviewer["avatar_url"],
+ "assigned_reviews": 1,
+ "finished_reviews": 0,
+ "last_review_date": "0000-00-00T00:00:00Z"
+ }
+
+ # if filter is set, only count reviews after earliest_date
+ if earliest_date and pull["created_at"] > earliest_date:
+ if reviewer_login in whitelist:
+ current_dict = reviewers_filter_whitelist
+ else:
+ current_dict = reviewers_filter
+
+ if reviewer_login in current_dict:
+ current_dict[reviewer_login]["assigned_reviews"] += 1
+ else:
+ current_dict[reviewer_login] = {
+ "avatar_url": reviewer["avatar_url"],
+ "assigned_reviews": 1,
+ "finished_reviews": 0,
+ "last_review_date": "0000-00-00T00:00:00Z"
+ }
+
+ # Get reviews for the pull request, but count only once per PR
+ pull_reviews_url = pull["url"] + "/reviews"
+ pull_reviews, _ = get_api_response_wait(pull_reviews_url)
+ local_reviewers = {} # prevent double counting
+ local_reviewers_filter = {} # prevent double counting
+
+ for review in pull_reviews:
+ reviewer_login = review["user"]["login"]
+ date = review["submitted_at"]
+
+ if reviewer_login in whitelist:
+ current_dict = reviewers_whitelist
+ else:
+ current_dict = reviewers
+
+ if reviewer_login in current_dict:
+ if reviewer_login not in local_reviewers:
+ current_dict[reviewer_login]["assigned_reviews"] += 1
+ current_dict[reviewer_login]["finished_reviews"] += 1
+ local_reviewers[reviewer_login] = True
+ if date > current_dict[reviewer_login]["last_review_date"]:
+ current_dict[reviewer_login]["last_review_date"] = date
+ else:
+ current_dict[reviewer_login] = {
+ "avatar_url": review["user"]["avatar_url"],
+ "assigned_reviews": 1,
+ "finished_reviews": 1,
+ "last_review_date": date
+ }
+ local_reviewers[reviewer_login] = True
+
+ # if filter is set, only count reviews after earliest_date
+ if earliest_date and pull["created_at"] > earliest_date:
+ if reviewer_login in whitelist:
+ current_dict = reviewers_filter_whitelist
+ else:
+ current_dict = reviewers_filter
+
+ if reviewer_login in current_dict:
+ if reviewer_login not in local_reviewers_filter:
+ current_dict[reviewer_login]["assigned_reviews"] += 1
+ current_dict[reviewer_login]["finished_reviews"] += 1
+ local_reviewers_filter[reviewer_login] = True
+ if date > current_dict[reviewer_login]["last_review_date"]:
+ current_dict[reviewer_login]["last_review_date"] = date
+ else:
+ current_dict[reviewer_login] = {
+ "avatar_url": review["user"]["avatar_url"],
+ "assigned_reviews": 1,
+ "finished_reviews": 1,
+ "last_review_date": date
+ }
+ local_reviewers_filter[reviewer_login] = True
+
+ return reviewers, reviewers_whitelist, reviewers_filter, reviewers_filter_whitelist, ct_pull
+
+
+def create_reviewers_table_with_graph(reviewers_stats, user_names, table_name):
+ """
+ Creates an HTML table with reviewer statistics and graphs.
+
+ Args:
+ reviewers_stats (dict): A dictionary containing reviewer statistics.
+ The keys are reviewer names and the values are dictionaries
+ containing the following keys:
+ - 'avatar_url' (str): The URL of the reviewer's avatar image.
+ - 'assigned_reviews' (int): The number of reviews assigned to the reviewer.
+ - 'finished_reviews' (int): The number of reviews finished by the reviewer.
+ - 'last_review_date' (str): The date of the last review by the reviewer.
+ user_names (dict): A dictionary mapping reviewer names to their corresponding user names.
+ table_name (str): The ID of the HTML table.
+
+ Returns:
+ str: The HTML content of the table with reviewer statistics and graphs.
+
+ style sheet for the table, copy into css file:
+
+
+ """
+
+ html_content = f"""
+
+
+
+
+ Reviewers' Stats
+
+
+
+
+
+
+
+
+
+ |
+ Reviewer |
+ Assigned |
+ Finished |
+ Rate |
+
+
+
+
+ """
+ if reviewers_stats:
+ # Find the reviewer with the highest number of finished reviews
+ max_finished_reviews = max(stats['finished_reviews'] for stats in reviewers_stats.values())
+
+ # Sort reviewers by finished reviews
+ sorted_reviewers = sorted(reviewers_stats.items(), key=lambda x: x[1]['finished_reviews'], reverse=True)
+
+ for idx, (reviewer, stats) in enumerate(sorted_reviewers):
+ finished_reviews_bar_len = (stats['finished_reviews'] / max_finished_reviews) * 100
+ finished_reviews_ratio = stats['finished_reviews']/stats['assigned_reviews']
+ finished_reviews_ratio_bar_len = (finished_reviews_ratio) * 100
+
+ # Add emojis for the first three reviewers
+ medal = ""
+ if idx == 0:
+ medal = "🥇"
+ elif idx == 1:
+ medal = "🥈"
+ elif idx == 2:
+ medal = "🥉"
+
+ html_content += f"""
+
+ {medal} |
+
+
+
+
+
+
+
+ |
+ {stats['assigned_reviews']} |
+ {stats['finished_reviews']}
+
+ |
+ {finished_reviews_ratio:.2f}
+
+ |
+
+
+ """
+
+ html_content += f"""
+
+
+ Fetched on {current_date.strftime("%Y-%m-%d %H:%M:%S")} UTC
+
+ """
+ html_content +=f"""
+
+
+
+ """
+
+ return html_content
+
+
+def print_reviewers_stats(reviewers_stats):
+ """
+ Prints the statistics of the reviewers.
+
+ Args:
+ reviewers_stats (dict): A dictionary containing the statistics of the reviewers.
+
+ Returns:
+ None
+ """
+ for reviewer, stats in sorted(reviewers_stats.items(), key=lambda x: x[1]['finished_reviews'], reverse=True)[:10]:
+ print(f"Reviewer: {reviewer}, Assigned Reviews: {stats['assigned_reviews']}, Finished Reviews: {stats['finished_reviews']}, rate of finished: {stats['finished_reviews']/stats['assigned_reviews']:.2f}, Last Review Date: {stats['last_review_date']}")
+
+
+# Replace with your GitHub repository owner and name
+owner = "ros-controls"
+repos = [
+ "ros2_control",
+ "ros2_controllers",
+ "ros2_control_demos",
+ "control_toolbox",
+ "realtime_tools",
+ "control_msgs",
+ "control.ros.org",
+ "gazebo_ros2_control",
+ "gz_ros2_control",
+ "kinematics_interface"
+]
+
+branches = {
+ "ros2_control": "master",
+ "ros2_controllers": "master",
+ "ros2_control_demos": "master",
+ "control_toolbox": "ros2-master",
+ "realtime_tools": "master",
+ "control_msgs": "master",
+ "control.ros.org": "master",
+ "gazebo_ros2_control": "master",
+ "gz_ros2_control": "master",
+ "kinematics_interface": "master"
+}
+
+maintainers = ["bmagyar", "destogl", "christophfroehlich"]
+
+# Get the current date and time
+current_date = datetime.utcnow()
+
+# Calculate one year ago from the current date
+one_year_ago = current_date - timedelta(days=365)
+
+# Format the date string as "YYYY-MM-DDTHH:MM:SSZ"
+formatted_date = one_year_ago.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+print("----------------------------------")
+print("------------ Start -------------")
+limit, reset = get_api_limit();
+print(f"API limit: {limit}, next reset: {datetime.fromtimestamp(reset)}")
+print("----------------------------------")
+print(f"Fetch pull requests, all-time and after {formatted_date}:")
+reviewers_stats, maintainers_stats, reviewers_stats_recent, maintainers_stats_recent, ct_pulls = get_reviewers_stats(owner, repos, branches, maintainers, formatted_date)
+print("----------------------------------")
+print("------------ Get User ------------")
+print("----------------------------------")
+unique_reviewers = set(
+ list(reviewers_stats_recent.keys())
+ + list(maintainers_stats_recent.keys())
+ + list(reviewers_stats.keys())
+ + list(maintainers_stats.keys())
+ )
+user_names = {}
+for reviewer_login in unique_reviewers:
+ user_names[reviewer_login] = get_user_name(reviewer_login)
+
+print(f"Got {len(unique_reviewers)} user names")
+
+# Print the reviewers' stats in a nice format
+print(f"---------------------------------")
+print(f"------ Results from {ct_pulls} PRs ------")
+print(f"---------------------------------")
+print(f"Reviewers' Stats, after {formatted_date}:")
+print("---------- maintainers -----------")
+print_reviewers_stats(maintainers_stats_recent)
+
+print("-------- not maintainers ---------")
+print_reviewers_stats(reviewers_stats_recent)
+
+print(f"Reviewers' Stats, all-time:")
+print("---------- maintainers -----------")
+print_reviewers_stats(maintainers_stats)
+
+print("-------- not maintainers ---------")
+print_reviewers_stats(reviewers_stats)
+print("----------------------------------")
+limit, reset = get_api_limit();
+print(f"API limit remaining: {limit}, next reset: {datetime.fromtimestamp(reset)}")
+print("----------------------------------")
+print("--------------- END --------------")
+print("----------------------------------")
+
+# Create the HTML content
+html_maintainers_stats_recent = create_reviewers_table_with_graph(maintainers_stats_recent, user_names, "maintainers_stats_recent")
+html_reviewers_stats_recent = create_reviewers_table_with_graph(reviewers_stats_recent, user_names, "reviewers_stats_recent")
+html_maintainers_stats = create_reviewers_table_with_graph(maintainers_stats, user_names, "maintainers_stats")
+html_reviewers_stats = create_reviewers_table_with_graph(reviewers_stats, user_names, "reviewers_stats")
+
+# Save the HTML content to a file named "reviewers_stats_with_graph.html"
+home_directory = os.path.expanduser( '~' )
+filename_maintainers_stats_recent = os.path.join(home_directory, 'reviews', 'maintainers_stats_recent.html')
+filename_reviewers_stats_recent = os.path.join(home_directory, 'reviews', 'reviewers_stats_recent.html')
+filename_maintainers_stats = os.path.join(home_directory, 'reviews', 'maintainers_stats.html')
+filename_reviewers_stats = os.path.join(home_directory, 'reviews', 'reviewers_stats.html')
+os.makedirs(os.path.dirname(filename_maintainers_stats_recent), exist_ok=True)
+
+with open(filename_maintainers_stats_recent, 'w') as file:
+ file.write(html_maintainers_stats_recent)
+print(f"HTML file {filename_maintainers_stats_recent} has been created.")
+
+with open(filename_reviewers_stats_recent, 'w') as file:
+ file.write(html_reviewers_stats_recent)
+print(f"HTML file {filename_reviewers_stats_recent} has been created.")
+
+with open(filename_maintainers_stats, 'w') as file:
+ file.write(html_maintainers_stats)
+print(f"HTML file {filename_maintainers_stats} has been created.")
+
+with open(filename_reviewers_stats, 'w') as file:
+ file.write(html_reviewers_stats)
+print(f"HTML file {filename_reviewers_stats} has been created.")
diff --git a/make_help_scripts/deploy_defines b/make_help_scripts/deploy_defines
index 7e119c94a24..ecf94160bd3 100755
--- a/make_help_scripts/deploy_defines
+++ b/make_help_scripts/deploy_defines
@@ -10,6 +10,28 @@ base_dir="$(dirname "$script_base_dir")"
base_branch="master"
build_dir="_build"
+# reviewer stats
+reviewer_stats_files=("maintainers_stats_recent.html"
+ "reviewers_stats_recent.html"
+ "maintainers_stats.html"
+ "reviewers_stats.html")
+reviewer_stats_cache_folder="reviews"
+reviewer_stats_target_folder="./doc/acknowledgements"
+
+add_reviewer_stats_file () {
+ for reviewer_stats_filename in "${reviewer_stats_files[@]}"; do
+ ORIGFILE="$HOME/$reviewer_stats_cache_folder/$reviewer_stats_filename"
+ # echo $ORIGFILE
+ if test -f "$ORIGFILE"; then
+ echo "Copy reviewer stats file '${reviewer_stats_filename}' to '${reviewer_stats_target_folder}'"
+ cp ${ORIGFILE} ${reviewer_stats_target_folder}
+ else
+ echo "Create empty reviewer stats file '${reviewer_stats_filename}' in '${reviewer_stats_target_folder}'"
+ echo "No review statistics available yet." > ${reviewer_stats_target_folder}/${reviewer_stats_filename}
+ fi
+ done
+}
+
# definition single html
# the branch from which the api is checked out and built
api_branch="master"
diff --git a/requirements.txt b/requirements.txt
index a3604d7c76f..cf3a9d66669 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ sphinx-copybutton
sphinx-multiversion
sphinx_rtd_theme
sphinx-tabs==3.4.1
+requests