|  | #!/usr/bin/env python3 | 
|  |  | 
|  | # | 
|  | # Copyright 2021, The Android Open Source Project | 
|  | # | 
|  | # 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. | 
|  | # | 
|  |  | 
|  | """Repohook script to run checks on TODOs in CHRE. | 
|  |  | 
|  | This script runs the following checks on TODOs in a commit: | 
|  |  | 
|  | 1: Prints a warning if a TODO references the bug ID in the commit message. | 
|  | This is mainly intended to minimize TODOs in the CHRE codebase, and to be | 
|  | an active reminder to remove a TODO once a commit addresses the debt | 
|  | mentioned. | 
|  |  | 
|  | 2: Fails the repo upload if the current commit adds a TODO, but fails to | 
|  | associate it with a bug-ID in the (usual) expected format of | 
|  | 'TODO(b/13371337). | 
|  |  | 
|  | A bug ID field in the commit message is REQUIRED for this script to work. | 
|  | This can be ensured by adding a 'commit_msg_bug_field = true' hook to the | 
|  | project's PREUPLOAD.cfg file. It is also recommended to add the | 
|  | 'ignore_merged_commits' option to avoid unexpected script behavior. | 
|  |  | 
|  | This script will work with any number of commits in the current repo | 
|  | checkout. | 
|  | """ | 
|  |  | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  | COMMIT_HASH = os.environ['PREUPLOAD_COMMIT'] | 
|  |  | 
|  | # According to the repohooks documentation, only the warning and success IDs | 
|  | # are mentioned - we use a random non-zero value (that's high enough to | 
|  | # avoid confusion with errno values) as our error code. | 
|  | REPO_ERROR_RETURN_CODE = 1337 | 
|  | REPO_WARNING_RETURN_CODE = 77 | 
|  | REPO_SUCCESS_RETURN_CODE = 0 | 
|  |  | 
|  | def check_for_unassociated_todos() -> int: | 
|  | """Check if a TODO has a bug ID associated with it. | 
|  |  | 
|  | Check if a TODO has a bug ID, in the usual 'TODO(b/13371337): {desc}' | 
|  | format. Also prints the line where said TODO was found. | 
|  |  | 
|  | Returns: | 
|  | An error code if a TODO has no bugs associated with it. | 
|  | """ | 
|  | rc = REPO_SUCCESS_RETURN_CODE | 
|  | commit_contents_cmd = 'git diff ' + COMMIT_HASH + '~ ' + COMMIT_HASH | 
|  | diff_result_lines = subprocess.check_output(commit_contents_cmd, | 
|  | shell=True, | 
|  | encoding='UTF-8') \ | 
|  | .split('\n') | 
|  | regex = r'TODO\(b\/([0-9]+)(?=[^\/]*$)' | 
|  |  | 
|  | for line in diff_result_lines: | 
|  | if line.startswith('+') and not line.startswith('+++') and \ | 
|  | 'TODO' in line and not re.findall(regex, line): | 
|  | print('Found a TODO in the following line in the commit without an \ | 
|  | associated bug-ID!') | 
|  | print(line) | 
|  | print('Please include a bug ID in the format TODO(b/13371337)') | 
|  | rc = REPO_ERROR_RETURN_CODE | 
|  |  | 
|  | return rc | 
|  |  | 
|  | def grep_for_todos(bug_id : str) -> int: | 
|  | """Searches for TODOs associated with the BUG ID referenced in the commit. | 
|  |  | 
|  | Args: | 
|  | bug_id: Bug ID referenced in the commit. | 
|  |  | 
|  | Returns: | 
|  | A warning code if current bug ID references any TODOs. | 
|  | """ | 
|  | grep_result = None | 
|  | rc = REPO_SUCCESS_RETURN_CODE | 
|  | git_repo_path_cmd = 'git rev-parse --show-toplevel' | 
|  | repo_path = ' ' + subprocess.check_output(git_repo_path_cmd, shell=True, | 
|  | encoding='UTF-8') | 
|  |  | 
|  | grep_base_cmd = 'grep -nri ' | 
|  | grep_file_filters = '--include \*.h --include \*.cc --include \*.cpp --include \*.c ' | 
|  | grep_shell_cmd = grep_base_cmd + grep_file_filters + bug_id + repo_path | 
|  | try: | 
|  | grep_result = subprocess.check_output(grep_shell_cmd, shell=True, | 
|  | encoding='UTF-8') | 
|  | except subprocess.CalledProcessError as e: | 
|  | if e.returncode != 1: | 
|  | # A return code of 1 means that grep returned a 'NOT_FOUND', which is | 
|  | # our ideal scenario! A return code of > 1 means something went very | 
|  | # wrong with grep. We still return a success here, since there's | 
|  | # nothing much else we can do (and this tool is intended to be mostly | 
|  | # informational). | 
|  | print('ERROR: grep failed with err code {}'.format(e.returncode), | 
|  | file=sys.stderr) | 
|  | print('The grep command that was run was:\n{}'.format(grep_shell_cmd), | 
|  | file=sys.stderr) | 
|  |  | 
|  | if grep_result is not None: | 
|  | print('Matching TODOs found for the Bug-ID in the commit message..') | 
|  | print('Hash of the current commit being checked: {}' | 
|  | .format(COMMIT_HASH)) | 
|  | grep_result = grep_result.replace(repo_path + '/', '') | 
|  | print(grep_result) | 
|  | rc = REPO_WARNING_RETURN_CODE | 
|  |  | 
|  | return rc | 
|  |  | 
|  | def get_bug_id_for_current_commit() -> str: | 
|  | """Get the Bug ID for the current commit | 
|  |  | 
|  | Returns: | 
|  | The bug ID for the current commit. | 
|  | """ | 
|  | git_current_commit_msg_cmd = 'git log --format=%B -n 1 ' | 
|  | commit_msg_lines_cmd = git_current_commit_msg_cmd + COMMIT_HASH | 
|  | commit_msg_lines_list = subprocess.check_output(commit_msg_lines_cmd, | 
|  | shell=True, | 
|  | encoding='UTF-8') \ | 
|  | .split('\n') | 
|  | try: | 
|  | bug_id_line = \ | 
|  | [line for line in commit_msg_lines_list if \ | 
|  | any(word in line.lower() for word in ['bug:', 'fixes:'])][0] | 
|  | except IndexError: | 
|  | print('Please include a Bug or Fixes field in the commit message') | 
|  | sys.exit(-1); | 
|  | return bug_id_line.split(':')[1].strip() | 
|  |  | 
|  | def is_file_in_diff(filename : str) -> bool: | 
|  | """Check if a given filename is part of the commit. | 
|  |  | 
|  | Args: | 
|  | filename: filename to check in the git diff. | 
|  |  | 
|  | Returns: | 
|  | True if the file is part of the commit. | 
|  | """ | 
|  | commit_contents_cmd = 'git diff ' + COMMIT_HASH + '~ ' + COMMIT_HASH | 
|  | diff_result = subprocess.check_output(commit_contents_cmd, shell=True, | 
|  | encoding='UTF-8') | 
|  | return filename in diff_result | 
|  |  | 
|  | def main(): | 
|  | # This script has a bunch of TODOs peppered around, though not with the | 
|  | # same intention as the checks that are being performed. Skip the checks | 
|  | # if we're committing changes to this script! One caveat is that we | 
|  | # should avoid pushing in changes to other code if we're committing | 
|  | # changes to this script. | 
|  | rc = REPO_SUCCESS_RETURN_CODE | 
|  | if not is_file_in_diff(os.path.basename(__file__)): | 
|  | bug_id = get_bug_id_for_current_commit() | 
|  | grep_rc = grep_for_todos(bug_id) | 
|  | check_rc = check_for_unassociated_todos() | 
|  | rc = max(grep_rc, check_rc) | 
|  | sys.exit(rc) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main() |