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 + + + + + +

+ + + + + + + + + + + + + """ + 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""" + + + + + + + + + """ + + html_content += f""" + +
ReviewerAssignedFinishedRate
{medal} +
+
+ {reviewer} +
+
+
+ {user_names[reviewer]}
+ {reviewer}{reviewer} +
+
+
+
{stats['assigned_reviews']}{stats['finished_reviews']} +
+
+
+
{finished_reviews_ratio:.2f} +
+
+
+
+ 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