1#!/usr/bin/env python3 2 3# 4# Copyright 2021, The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19"""Repohook script to run checks on TODOs in CHRE. 20 21This script runs the following checks on TODOs in a commit: 22 231: Prints a warning if a TODO references the bug ID in the commit message. 24This is mainly intended to minimize TODOs in the CHRE codebase, and to be 25an active reminder to remove a TODO once a commit addresses the debt 26mentioned. 27 282: Fails the repo upload if the current commit adds a TODO, but fails to 29associate it with a bug-ID in the (usual) expected format of 30'TODO(b/13371337). 31 32A bug ID field in the commit message is REQUIRED for this script to work. 33This can be ensured by adding a 'commit_msg_bug_field = true' hook to the 34project's PREUPLOAD.cfg file. It is also recommended to add the 35'ignore_merged_commits' option to avoid unexpected script behavior. 36 37This script will work with any number of commits in the current repo 38checkout. 39""" 40 41import os 42import re 43import subprocess 44import sys 45 46COMMIT_HASH = os.environ['PREUPLOAD_COMMIT'] 47 48# According to the repohooks documentation, only the warning and success IDs 49# are mentioned - we use a random non-zero value (that's high enough to 50# avoid confusion with errno values) as our error code. 51REPO_ERROR_RETURN_CODE = 1337 52REPO_WARNING_RETURN_CODE = 77 53REPO_SUCCESS_RETURN_CODE = 0 54 55def check_for_unassociated_todos() -> int: 56 """Check if a TODO has a bug ID associated with it. 57 58 Check if a TODO has a bug ID, in the usual 'TODO(b/13371337): {desc}' 59 format. Also prints the line where said TODO was found. 60 61 Returns: 62 An error code if a TODO has no bugs associated with it. 63 """ 64 rc = REPO_SUCCESS_RETURN_CODE 65 commit_contents_cmd = 'git diff ' + COMMIT_HASH + '~ ' + COMMIT_HASH 66 diff_result_lines = subprocess.check_output(commit_contents_cmd, 67 shell=True, 68 encoding='UTF-8') \ 69 .split('\n') 70 regex = r'TODO\(b\/([0-9]+)' 71 72 for line in diff_result_lines: 73 if line.startswith('+') and not line.startswith('+++') and \ 74 'TODO' in line and not re.findall(regex, line): 75 print('Found a TODO in the following line in the commit without an \ 76 associated bug-ID!') 77 print(line) 78 print('Please include a bug ID in the format TODO(b/13371337)') 79 rc = REPO_ERROR_RETURN_CODE 80 81 return rc 82 83def grep_for_todos(bug_id : str) -> int: 84 """Searches for TODOs associated with the BUG ID referenced in the commit. 85 86 Args: 87 bug_id: Bug ID referenced in the commit. 88 89 Returns: 90 A warning code if current bug ID references any TODOs. 91 """ 92 grep_result = None 93 rc = REPO_SUCCESS_RETURN_CODE 94 git_repo_path_cmd = 'git rev-parse --show-toplevel' 95 repo_path = ' ' + subprocess.check_output(git_repo_path_cmd, shell=True, 96 encoding='UTF-8') 97 98 grep_base_cmd = 'grep -nri ' 99 grep_file_filters = '--include \*.h --include \*.cc --include \*.cpp --include \*.c ' 100 grep_shell_cmd = grep_base_cmd + grep_file_filters + bug_id + repo_path 101 try: 102 grep_result = subprocess.check_output(grep_shell_cmd, shell=True, 103 encoding='UTF-8') 104 except subprocess.CalledProcessError as e: 105 if e.returncode != 1: 106 # A return code of 1 means that grep returned a 'NOT_FOUND', which is 107 # our ideal scenario! A return code of > 1 means something went very 108 # wrong with grep. We still return a success here, since there's 109 # nothing much else we can do (and this tool is intended to be mostly 110 # informational). 111 print('ERROR: grep failed with err code {}'.format(e.returncode), 112 file=sys.stderr) 113 print('The grep command that was run was:\n{}'.format(grep_shell_cmd), 114 file=sys.stderr) 115 116 if grep_result is not None: 117 print('Matching TODOs found for the Bug-ID in the commit message..') 118 print('Hash of the current commit being checked: {}' 119 .format(COMMIT_HASH)) 120 grep_result = grep_result.replace(repo_path + '/', '') 121 print(grep_result) 122 rc = REPO_WARNING_RETURN_CODE 123 124 return rc 125 126def get_bug_id_for_current_commit() -> str: 127 """Get the Bug ID for the current commit 128 129 Returns: 130 The bug ID for the current commit. 131 """ 132 git_current_commit_msg_cmd = 'git log --format=%B -n 1 ' 133 commit_msg_lines_cmd = git_current_commit_msg_cmd + COMMIT_HASH 134 commit_msg_lines_list = subprocess.check_output(commit_msg_lines_cmd, 135 shell=True, 136 encoding='UTF-8') \ 137 .split('\n') 138 try: 139 bug_id_line = \ 140 [line for line in commit_msg_lines_list if \ 141 any(word in line.lower() for word in ['bug:', 'fixes:'])][0] 142 except IndexError: 143 print('Please include a Bug or Fixes field in the commit message') 144 sys.exit(-1); 145 return bug_id_line.split(':')[1].strip() 146 147def is_file_in_diff(filename : str) -> bool: 148 """Check if a given filename is part of the commit. 149 150 Args: 151 filename: filename to check in the git diff. 152 153 Returns: 154 True if the file is part of the commit. 155 """ 156 commit_contents_cmd = 'git diff ' + COMMIT_HASH + '~ ' + COMMIT_HASH 157 diff_result = subprocess.check_output(commit_contents_cmd, shell=True, 158 encoding='UTF-8') 159 return filename in diff_result 160 161def main(): 162 # This script has a bunch of TODOs peppered around, though not with the 163 # same intention as the checks that are being performed. Skip the checks 164 # if we're committing changes to this script! One caveat is that we 165 # should avoid pushing in changes to other code if we're committing 166 # changes to this script. 167 rc = REPO_SUCCESS_RETURN_CODE 168 if not is_file_in_diff(os.path.basename(__file__)): 169 bug_id = get_bug_id_for_current_commit() 170 grep_rc = grep_for_todos(bug_id) 171 check_rc = check_for_unassociated_todos() 172 rc = max(grep_rc, check_rc) 173 sys.exit(rc) 174 175if __name__ == '__main__': 176 main() 177